Пользовательский оператор преобразования из базового класса
Вступление
Мне известно, что "определенные пользователем преобразования в базовый класс или из него запрещены". В качестве объяснения этому правилу 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>
{
}