Ограничить тип возвращаемого значения подтипом этого

У меня есть следующий интерфейс:

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
        }
    }
    ...
}

почему просто instance.GetType() не вариант?

Ivan Petrov 24.06.2024 09:56

@IvanPetrov Я не понимаю, как instance.GetType() можно включить в ограничение типа. Можете ли вы уточнить?

Kjara 24.06.2024 09:59

у вас есть экземпляр, и вы хотите узнать его тип — разве не проще всего просто вызвать для него GetType? зачем вам вообще нужны перечисления?

Ivan Petrov 24.06.2024 10:02

@IvanPetrov Нет, у меня нет экземпляра. У меня есть json, который я хочу полиморфно десериализовать. Для этого мне нужны перечисления.

Kjara 24.06.2024 10:04

да, я думаю, мне удалось это усвоить.

Ivan Petrov 24.06.2024 10:04

Я думаю, это вопрос XY. Вот ответ на Х

Orace 24.06.2024 10:10

@Orace Спасибо за подсказку. Окончательно! Когда я писал свой код, System.Text.Json не мог этого сделать. К сожалению, мне еще нужно это решить, потому что нам придется работать с .net481.

Kjara 24.06.2024 10:17

Кстати, в этой реализации перечисления не являются допустимым дискриминатором типов

Orace 24.06.2024 10:19

как именно ты называешь TypeFromEnum?

Ivan Petrov 24.06.2024 10:22

@IvanPetrov Внутри моего самореализованного подкласса JsonConverter<BaseType> в методах Read и Write. public override void Write(Utf8JsonWriter writer, BaseType value, JsonSerializerOptions options) { var type = _TypeFromEnum(value.Type); ... }. Для Read вставлять это сюда слишком сложно/долго, поскольку это не тема моего вопроса.

Kjara 24.06.2024 10:25

значит, вы не вызываете метод статического интерфейса, например A.TypeFromEnum? Как вообще возможен вызов

Ivan Petrov 24.06.2024 10:27
TypeFromEnum возвращает экземпляр System.Type, который содержит информацию о типе во время выполнения. Невозможно наложить какие-либо ограничения во время компиляции.
Johnathan Barclay 24.06.2024 10:30

@IvanPetrov Ах, я половину забыл. _TypeFromEnum — свойство конвертера, которое задается при создании конвертера. Конвертер имеет параметры типа; среди них BaseType. Отсюда он получает статический метод TypeFromEnum и присваивает ему свое свойство _TypeFromEnum.

Kjara 24.06.2024 10:46

@Kjara, значит, вы передаете A как общий параметр BaseType и эффективно вызываете A.TypeFromEnum?

Ivan Petrov 24.06.2024 10:48

@ИванПетров да.

Kjara 24.06.2024 10:59

Почему вы не можете просто использовать метод интерфейса по умолчанию прямо в интерфейсе internal static Type TypeFromEnum() => return typeof(EnumType);

Charlieface 24.06.2024 10:59

@Charlieface, потому что значение типа перечисления содержит подтип, который у нас есть, нас не интересует его тип

Ivan Petrov 24.06.2024 11:00

@Kjara Я знаю, что вы уже приняли ответ, но я думаю, что мой на самом деле отвечает на вопрос с минимальными накладными расходами (в последней версии должно быть понятно, как ее использовать)

Ivan Petrov 24.06.2024 13:37
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
18
94
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Если мы полагаемся на оператор 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;
}

Другие вопросы по теме

Компиляция карты времени имени строки типа в типы в C++
Вычисление чисел, для которых требуется тип данных размером более 16 байт в C++
Введите 'строка | логическое значение не может быть назначено для ввода «никогда» в машинописном тексте
Почему тип int меняет размер в зависимости от архитектуры процессора, а другие типы — нет?
Определение макроса и использование типов данных для поиска абсолютного значения
Аргумент func с массивом смешанных типов (протокол), затем вызовите статический метод протокола
Не удалось преобразовать число с плавающей запятой 64 в число с плавающей запятой 32 в Python
Как перегрузить процедуру, объявленную в абстрактном типе в Фортране?
Разъяснение: «Значение значения определяется типом выражения, используемого для доступа к нему»
Как изменить неопределенный тип как необязательный в машинописном тексте?