Пользовательский оператор преобразования из базового класса

Вступление

Мне известно, что "определенные пользователем преобразования в базовый класс или из него запрещены". В качестве объяснения этому правилу MSDN дает: "Вам не нужен этот оператор".

Я понимаю, что пользовательское преобразование в базовый класс не требуется, поскольку это, очевидно, делается неявно. Однако мне нужно преобразование из базового класса.

В моем текущем проекте, в качестве оболочки неуправляемого кода, я использую указатель, хранящийся в классе Entity. Все классы, использующие указатель, происходят от этого класса Entity, например, класса Body.

Поэтому я имею:

Способ А

class Entity
{
    IntPtr Pointer;

    Entity(IntPtr pointer)
    {
        this.Pointer = pointer;
    }
}

class Body : Entity
{
    Body(IntPtr pointer) : base(pointer) { }

    explicit operator Body(Entity e)
    {
        return new Body(e.Pointer);
    }
}

Этот акт является незаконным. (Обратите внимание, что я не стал писать аксессоры). Без этого компилятор позволит мне сделать:

Метод Б

(Body)myEntity
...

Тем не менее, во время выполнения я получу исключение, говорящее, что это приведение невозможно.

Заключение

Поэтому здесь я нуждаюсь в пользовательском преобразовании из базового класса, и C# отказывает мне в этом. Используя метод A, компилятор будет жаловаться, но код будет логически работать во время выполнения. Используя метод B, компилятор не будет жаловаться, но код, очевидно, потерпит неудачу во время выполнения.

Что я нахожу странным в этой ситуации, так это то, что MSDN говорит мне, что мне не нужен этот оператор, и компилятор действует так, как будто это возможно неявно (метод B). Что я должен сделать?

Я знаю, что я могу использовать:

Решение А

class Body : Entity
{
    Body(IntPtr pointer) : base(pointer) { }

    static Body FromEntity(Entity e)
    {
        return new Body(e.Pointer);
    }
}

Решение Б

class Body : Entity
{
    Body(IntPtr pointer) : base(pointer) { }

    Body(Entity e) : base(e.Pointer) { }
}

Решение С

class Entity
{
    IntPtr Pointer;

    Entity(IntPtr pointer)
    {
        this.Pointer = pointer;
    }

    Body ToBody()
    {
        return new Body(this.Pointer);
    }
}

Но, честно говоря, все синтаксисы для них ужасны и на самом деле должны быть заброшены. Итак, есть ли способ заставить броски работать? Это недостаток дизайна C# или я упустил возможность? Это как если бы C# не доверял мне настолько, чтобы написать свое собственное преобразование из базы в ребенка, используя их систему приведения.

8 ответов

Решение

Это не недостаток дизайна. Вот почему:

Entity entity = new Body();
Body body = (Body) entity;

Если бы вам было позволено написать собственное пользовательское преобразование здесь, было бы два допустимых преобразования: попытка просто выполнить обычное приведение (которое является ссылочным преобразованием, сохранение идентичности) и ваше пользовательское преобразование.

Какой следует использовать? Вы бы действительно хотели, чтобы это делало разные вещи?

// Reference conversion: preserves identity
Object entity = new Body();
Body body = (Body) entity;

// User-defined conversion: creates new instance
Entity entity = new Body();
Body body = (Body) entity;

Юк! Так лежит безумие, ИМО. Не забывайте, что компилятор решает это во время компиляции, основываясь только на типах компиляции участвующих выражений.

Лично я бы пошел с решением C - и, возможно, даже сделать его виртуальным методом. Сюда Body может переопределить это просто вернуться this, если вы хотите, чтобы это было сохранение идентичности, где это возможно, но создание нового объекта, где это необходимо.

Хорошо, когда вы кастуете Entity в Bodyвы на самом деле не кастуете друг друга, а скорее кастуете IntPtr к новой сущности.

Почему бы не создать явный оператор преобразования из IntPtr?

public class Entity {
    public IntPtr Pointer;

    public Entity(IntPtr pointer) {
        this.Pointer = pointer;
    }
}

public class Body : Entity {
    Body(IntPtr pointer) : base(pointer) { }

    public static explicit operator Body(IntPtr ptr) {
        return new Body(ptr);
    }

    public static void Test() {
        Entity e = new Entity(new IntPtr());
        Body body = (Body)e.Pointer;
    }
}

Вы должны использовать свое решение B (аргумент конструктора); во-первых, вот почему не использовать другие предложенные решения:

  • Решение A - это просто оболочка для решения B;
  • Решение C просто неверно (почему базовый класс должен знать, как преобразовать себя в любой подкласс?)

Кроме того, если Body класс должен был содержать дополнительные свойства, к чему они должны быть инициализированы при выполнении приведения? Гораздо лучше использовать конструктор и инициализировать свойства подкласса, как это принято в языках ОО.

Причина, по которой вы не можете этого сделать, заключается в том, что в общем случае это небезопасно. Рассмотрим возможности. Если вы хотите сделать это, потому что базовый и производный класс являются взаимозаменяемыми, то у вас действительно есть только один класс, и вы должны объединить два. Если вы хотите иметь оператор приведения для удобства понижения базы до производной, вам следует учитывать, что не каждая переменная, указанная в качестве базового класса, будет указывать на экземпляр конкретного производного класса, который вы пытаетесь привести к его преобразованию. к. Это может быть так, но вам придется сначала проверить или рискнуть получить недопустимое исключение приведения. Вот почему унижение вообще не одобряется, и это не что иное, как понижение в сопротивлении. Я предлагаю вам пересмотреть свой дизайн.

Как насчет:

public class Entity {...}

public class Body : Entity
{
  public Body(Entity sourceEntity) { this.Pointer = sourceEntity.Pointer; }
}

поэтому в коде вам не нужно писать:

Body someBody = new Body(previouslyUnknownEntity.Pointer);

но вы можете использовать

Body someBody = new Body(previouslyUnknownEntity);

вместо.

Это просто косметическое изменение, я знаю, но это довольно ясно, и вы можете легко изменить внутреннее устройство. Он также используется в шаблоне обертки, имя которого я не могу вспомнить (для слегка отличных целей).
Также ясно, что вы создаете новую сущность из предоставленной, поэтому не следует путать, как будет оператор / преобразование.

Примечание: не использовал компилятор, поэтому существует возможность опечатки.

(Вызов протоколов некромантии...)

Вот мой вариант использования:

class ParseResult
{
    public static ParseResult Error(string message);
    public static ParseResult<T> Parsed<T>(T value);

    public bool IsError { get; }
    public string ErrorMessage { get; }
    public IEnumerable<string> WarningMessages { get; }

    public void AddWarning(string message);
}

class ParseResult<T> : ParseResult
{
    public static implicit operator ParseResult<T>(ParseResult result); // Fails
    public T Value { get; }
}

...

ParseResult<SomeBigLongTypeName> ParseSomeBigLongTypeName()
{
    if (SomethingIsBad)
        return ParseResult.Error("something is bad");
    return ParseResult.Parsed(new SomeBigLongTypeName());
}

Вот Parsed() могу сделать вывод T из его параметра, но Error не может, но может вернуть тип ParseResult который конвертируется в ParseResult<T> - или было бы, если бы не эта ошибка. Исправление заключается в возврате и преобразовании из подтипа:

class ParseResult
{
    public static ErrorParseResult Error(string message);
    ...
}

class ErrorParseResult : ParseResult {}

class ParseResult<T>
{
    public static implicit operator ParseResult<T>(ErrorParseResult result);
    ...
}

И все счастливы!

Честно говоря, я думаю, что первоначальный запрос неправильно понимают.

Рассмотрим простую ситуацию, когда базовый класс просто служит группой связанных классов.

Пример:

class Parent ...
class Child1 : Parent ...
class Child2 : Parent ...

Где программист знает, как явно преобразовать один дочерний класс в другой.

В Parent class можно использовать, например, в:

Dictionary<string, Parent>

Я думаю, что исходный запрос просит:

Как уметь кодировать:

Class1 v1 = ...
Class2 v2 = ...

v1 = v2;

Где внутри Parent есть явный код для преобразования из Class2 к Class1 объекты.

В моем коде есть именно такая ситуация.

Лучшее, что мне удалось сделать, это добавить недвижимость в Parent класс, который знает, как выполнять преобразование, и возвращает правильный типизированный объект.

Это заставляет меня написать свой код:

v1 = v2.AsClass1;

Где недвижимость AsClass1 в Parent знает, как выполнить фактическое преобразование из Class2 к Class1.

Честно говоря, это кладж кода (он уродлив; он снижает простоту, может делать выражения до смешного длинными, запутывает и, что самое досадное, ему не хватает элегантности), но это лучшее, что я мог придумать.

И, да, как вы уже догадались, Parent класс также включает AsClass2 метод:-)

Все, что я хотел сделать, это:

v1 = v2;

и заставить компилятор молча вызывать указанный мной метод для преобразования.

Я действительно не понимаю, почему компилятор это поддерживает:-(

На мой взгляд, это действительно ничем не отличается:

int v1;
decimal v2;
. . .
v1 = (int)v2;

Компилятор умеет незаметно вызывать какой-нибудь встроенный метод преобразования.

Это старое обсуждение, но я просто хотел добавить немного собственного опыта.

В библиотеке классов у нас когда-то были математические объекты, такие как 2D-точки И 2D-векторы. Поскольку характеристики объектов обоих классов в основном одинаковы (хотя и не совсем одинаковы, поэтому были необходимы оба класса), идея заключалась в том, чтобы определитьVector2D и получить Point2Dот него. Это избавило бы от множества повторяющихся определений, но пользовательский оператор преобразования из вектора в точку был бы невозможен.

Так как мы сильно хотели сознательно поменять типы в коде, мы просто решили отказаться от идеи наследования, объявили оба класса независимо и ввели неявные операторы преобразования. Тогда мы могли бы свободно обменивать оба типа в коде.

Кажется, что равенство ссылок не было вашей заботой, тогда вы можете сказать:

  • Код

    public class Entity {
        public sealed class To<U> where U : Entity {
            public static implicit operator To<U>(Entity entity) {
                return new To<U> { m_handle=entity.Pointer };
            }
    
            public static implicit operator U(To<U> x) {
                return (U)Activator.CreateInstance(typeof(U), x.m_handle);
            }
    
            To() { // not exposed
            }
    
            IntPtr m_handle; // not exposed
        }
    
        IntPtr Pointer; // not exposed
    
        public Entity(IntPtr pointer) {
            this.Pointer=pointer;
        }
    }
    

    public class Body:Entity {
        public Body(IntPtr pointer) : base(pointer) {
        }
    }
    
    // added for the extra demonstration
    public class Context:Body {
        public Context(IntPtr pointer) : base(pointer) {
        }
    }
    

и

  • Тестовое задание

    public static class TestClass {
        public static void TestMethod() {
            Entity entity = new Entity((IntPtr)0x1234);
            Body body = (Entity.To<Body>)entity;
            Context context = (Body.To<Context>)body;
        }
    }
    

Вы не написали методы доступа, но я учел инкапсуляцию, чтобы не показывать их указатели. Под капотом этой реализации используется промежуточный класс, который не входит в цепочку наследования, а цепочку преобразования.

Activator участие здесь хорошо, чтобы не добавлять дополнительные new() ограничение как U уже ограничены Entity и иметь параметризованный конструктор. To<U> хотя он открыт, но запечатан без предоставления своего конструктора, он может быть создан только из оператора преобразования.

В тестовом коде сущность фактически преобразована в общий To<U> объект, а затем тип цели, поэтому дополнительная демонстрация от body в context, Так как To<U> это вложенный класс, он может получить доступ к частному Pointer из содержащего класса, таким образом, мы можем выполнить вещи, не подвергая указатель.

Ну вот и все.

Угу, я закончил тем, что просто использовал простой метод Cast() внутри моей модифицированной сущности. Возможно, я просто упускаю суть, но мне нужно изменить базовый тип, чтобы я мог сохранить свой код внутри нового объекта для выполнения x. Я бы использовал публичный статический явный оператор, если бы компилятор позволил мне. Наследование портит явный оператор приведения.

Применение:

var newItem = UpgradedEnity(dbItem);
var stuff = newItem.Get();

Образец:

public class UpgradedEnity : OriginalEnity_Type
    {
        public string Get()
        {
            foreach (var item in this.RecArray)
            {
                //do something
            }
            return "return something";
        }

        public static UpgradedEnity Cast(OriginalEnity_Type v)
        {
            var rv = new UpgradedEnity();
            PropertyCopier<OriginalEnity_Type, UpgradedEnity>.Copy(v, rv);
            return rv;
        }

        public class PropertyCopier<TParent, TChild> where TParent : class
                                            where TChild : class
        {
            public static void Copy(TParent from, TChild to)
            {
                var parentProperties = from.GetType().GetProperties();
                var childProperties = to.GetType().GetProperties();

                foreach (var parentProperty in parentProperties)
                {
                    foreach (var childProperty in childProperties)
                    {
                        if (parentProperty.Name == childProperty.Name && parentProperty.PropertyType == childProperty.PropertyType)
                        {
                            childProperty.SetValue(to, parentProperty.GetValue(from));
                            break;
                        }
                    }
                }
            }
        }
    }

Вы можете использовать общий, его можно, как удар

public class a<based>
    {
        public static implicit operator b(a<based> v)
        {
            return new b();
        }
    }

    public class b
        : a<b>
    {
    }
Другие вопросы по тегам