У меня есть следующий интерфейс:
internal interface ITyped<EnumType> where EnumType : struct, Enum
{
public EnumType Type { get; }
// !!! this method must always return a subtype* of the implementing class
internal static Type TypeFromEnum(EnumType value) => throw new NotImplementedException();
}
Мне нужен метод TypeFromEnum
, чтобы всегда возвращать тип, который является подтипом* реализующего класса. Есть ли способ записать это условие в коде, возможно, с каким-то ограничением where
?
(*) Под «подтипом» я подразумеваю либо сам тип класса, либо настоящий дочерний тип, то есть что-то, что можно было бы захватить с помощью where Sub : Base
, если бы Sub
и Base
были параметрами типа.
Вариант использования: у меня есть несколько классов с одноуровневой иерархией наследования.
public class A { ... }
public class A1 : A { ... }
public class A2 : A { ... }
public class A3 : A { ... }
public abstract class B { ... }
public class B1 { ... }
public class B2 { ... }
и мне нужно (де-)сериализовать их из/в json, используя System.Text.Json
. Чтобы сделать это возможным, A
и B
имеют свойство Type
некоторого типа enum
. Для десериализации у меня есть собственный конвертер, который считывает содержимое этой записи type
как enum
, а затем вызывает TypeFromEnum
, чтобы определить, в какой тип следует десериализовать. Вот перечисления и соответствующее содержимое классов:
public enum AType { a, a1, a2, a3 }
public enum BType { b1, b2 }
public class A : ITyped<AEnum>
{
public AEnum Type { get; }
internal static Type TypeFromEnum(AEnum value)
{
switch (value)
{
case AType.a: return typeof(A);
case AType.a1: return typeof(A1);
case AType.a2: return typeof(A2);
case AType.a3: return typeof(A3);
default: throw new InvalidOperationException(); // should never be called
}
}
...
}
public abstract class B : ITyped<BEnum>
{
public BEnum Type { get; }
internal static Type TypeFromEnum(BEnum value)
{
switch (value)
{
case BType.b1: return typeof(B1);
case BType.b2: return typeof(B2);
default: throw new InvalidOperationException(); // should never be called
}
}
...
}
@IvanPetrov Я не понимаю, как instance.GetType()
можно включить в ограничение типа. Можете ли вы уточнить?
у вас есть экземпляр, и вы хотите узнать его тип — разве не проще всего просто вызвать для него GetType? зачем вам вообще нужны перечисления?
@IvanPetrov Нет, у меня нет экземпляра. У меня есть json, который я хочу полиморфно десериализовать. Для этого мне нужны перечисления.
да, я думаю, мне удалось это усвоить.
Я думаю, это вопрос XY. Вот ответ на Х
@Orace Спасибо за подсказку. Окончательно! Когда я писал свой код, System.Text.Json
не мог этого сделать. К сожалению, мне еще нужно это решить, потому что нам придется работать с .net481.
Кстати, в этой реализации перечисления не являются допустимым дискриминатором типов
как именно ты называешь TypeFromEnum
?
@IvanPetrov Внутри моего самореализованного подкласса JsonConverter<BaseType>
в методах Read
и Write
. public override void Write(Utf8JsonWriter writer, BaseType value, JsonSerializerOptions options) { var type = _TypeFromEnum(value.Type); ... }
. Для Read
вставлять это сюда слишком сложно/долго, поскольку это не тема моего вопроса.
значит, вы не вызываете метод статического интерфейса, например A.TypeFromEnum? Как вообще возможен вызов
TypeFromEnum
возвращает экземпляр System.Type
, который содержит информацию о типе во время выполнения. Невозможно наложить какие-либо ограничения во время компиляции.
@IvanPetrov Ах, я половину забыл. _TypeFromEnum
— свойство конвертера, которое задается при создании конвертера. Конвертер имеет параметры типа; среди них BaseType
. Отсюда он получает статический метод TypeFromEnum
и присваивает ему свое свойство _TypeFromEnum
.
@Kjara, значит, вы передаете A
как общий параметр BaseType и эффективно вызываете A.TypeFromEnum
?
@ИванПетров да.
Почему вы не можете просто использовать метод интерфейса по умолчанию прямо в интерфейсе internal static Type TypeFromEnum() => return typeof(EnumType);
@Charlieface, потому что значение типа перечисления содержит подтип, который у нас есть, нас не интересует его тип
@Kjara Я знаю, что вы уже приняли ответ, но я думаю, что мой на самом деле отвечает на вопрос с минимальными накладными расходами (в последней версии должно быть понятно, как ее использовать)
Если мы полагаемся на оператор switch-case в реализациях TypeFromEnum
, чтобы предоставить нам тип на основе значения перечисления (как это делает другой ответ), мы все равно можем обеспечить безопасность времени компиляции с этого момента, изменив подпись, чтобы вернуть обычай IAssignableTo<out T>
вместо Type
.
Для этого нам нужны два дополнительных типа:
public class TypeOf<T> : IAssignableTo<T> {
public Type Type => typeof(T);
}
public interface IAssignableTo<out T> {
Type Type { get; }
}
Они помогают нам использовать ковариацию интерфейса, которая соответствует вашим требованиям к «подтипам» (в примере ниже будет понятно, как это сделать).
Затем мы переписываем интерфейс, чтобы он мог принимать еще один общий параметр ClassType
, который мы ограничиваем типами, реализующими этот интерфейс. Это поможет нам изменить тип возвращаемого значения TypeFromEnum
на IAssignableTo<ClassType>
с Type
. Значение интерфейса IAssignableTo<ClassType>
имеет свойство Type, которое позволит нам получить экземпляр типа через typeof(ClassType)
(см. реализацию выше).
internal interface ITyped<EnumType, ClassType>
where EnumType : struct, Enum
where ClassType: ITyped<EnumType, ClassType> {
public EnumType Type { get; }
// !!! this method must always return a subtype* of the implementing class -> it does now
internal static IAssignableTo<ClassType> TypeFromEnum(EnumType value) => throw new NotImplementedException();
}
и реализация:
public enum AType { a, a1, a2, a3 }
public class B { }
public class A1 : A { }
public class A : ITyped<AType, A> {
public AType Type { get; }
internal static IAssignableTo<A> TypeFromEnum(AType value) {
switch (value) {
case AType.a: return new TypeOf<A>();
// IAssignableTo<A> covariance here allows us to return
// TypeOf<A1> for a value of IAssignableTo<A>
case AType.a1: return new TypeOf<A1>();
//CS0266 Cannot implicitly convert type 'TypeOf<B>'
// to 'IAssignableTo<A>'.
// An explicit conversion exists (are you missing a cast?)
case AType.a2: return new TypeOf<B>(); // compile time error
default: throw new InvalidOperationException(); // should never be called
}
}
}
Для использования вам просто нужно вызвать свойство Type
возвращаемого значения:
IAssignableTo<A> typeOf = A.TypeFromEnum(AType.a1);
Type aType = typeOf.Type;
в вашем случае использования, возможно, где A
- это общий параметр BaseType
для другого класса:
IAssignableTo<BaseType> typeOf = _TypeFromEnum(value.Type);
Type baseType = typeOf.Type;
это помогает нам исключить проверку времени выполнения из другого ответа:
public Type SafeTypeFromEnum(EnumTypevalue value)
{
var result = TypeFromEnum(value);
if (!result.IsSubclassOf(typeof(EnumTypevalue)))
throw new Exception();
return result;
}
Невозможно заставить метод вернуть переменную, соответствующую определенному критерию.
Это похоже на этот интерфейс:
int GetOddRandomNumber();
Реализация может быть:
public int GetOddRandomNumber() => 42;
И ты ничего не можешь с этим поделать.
Решение состоит в том, чтобы обернуть вызов в метод, который будет проверять вывод:
public Type SafeTypeFromEnum(EnumTypevalue value)
{
var result = TypeFromEnum(value);
if (!result.IsSubclassOf(typeof(EnumTypevalue)))
throw new Exception();
return result;
}
почему просто
instance.GetType()
не вариант?