Как указать тип параметра как тип производного класса при переопределении абстрактной функции, где параметр является типом базового класса?

Я хотел бы сохранить экземпляры SpecificResult (и другие типы, производные от Result) в файле List<Result>. Каждый производный тип может содержать разные свойства. Я хотел бы иметь метод виртуального базового класса LimitsPassed на Result, который проверяет, соответствуют ли эти конкретные свойства некоторым критериям (например, значениям свойств из производных экземпляров Limits, которые передаются в качестве параметра методу). Разные классы, производные от Results, должны получать разные типы Limits.

Я попытался переопределить метод LimitsPassed с помощью более производного параметра SpecificLimits.

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

Каковы возможные решения для этого?

public abstract class Result
{
    public abstract bool LimitsPassed(Limits limits)
}

public class SpecificResult : Result
{
    public double MeasureA { get; set; }
    public string MeasureB { get; set; }

    public override bool LimitsPassed(SpecificLimits limits) 
    {
        //specify parameter base type to particular type of derived class

        return this.MeasureA >= limits.MeasureA_min
                && this.MeasureA <= limits.MeasureA_max
                && this.MeasureB == limits.MeasureB_master;
    }
}

public abstract class Limits
{
}

public class SpecificLimits : Limits
{
    public double MeasureA_min { get; set; }
    public double MeasureA_max { get; set; }

    public string MeasureB_master { get; set; }
}

Похоже, ваш класс Result, вероятно, должен быть универсальным по типу ограничения, которое он может получить. (например, class Result<TLimit> where TLimit : Limits). Тогда SpecificResult : Result<SpecificLimits>...

Jon Skeet 14.08.2024 15:27

Если я могу немного оспорить ваш дизайн... Что происходит, когда вы звоните LimitsPassed на SpecificResult и предоставляете Limits, которые не являются SpecificLimits? частью работы SpecificResult (в силу того факта, что это Result), так это то, что он должен обеспечивать реализацию LimitsPassed(Limits) Какова связь между SpecificResult и SpecificLimits? Другими словами, почему SpecificResult волнует, какой тип Limits передается, если это Result?

Wyck 14.08.2024 15:43

@Wyck Я хотел бы сохранить конкретные результаты (и другие типы, полученные из результатов) в списке <Results>. Каждый производный тип может содержать разные свойства. Метод LimitsPassed должен проверять, соответствуют ли эти свойства некоторым критериям (например, ограничениям). Различные классы, производные от результатов, будут иметь разные свойства и должны получать разные типы ограничений. И, наконец, LimitsPassed реализует логику для проверки соответствия свойств требованиям.

oleq 14.08.2024 15:57

С точки зрения дизайна, это не похоже на SpecificLimits, это Limits, потому что вы говорите, что «Различные классы, производные от результатов, будут иметь разные свойства». Я говорю, что, возможно, LimitsPassed(Limits limits) не следует быть членом Result. Ваш вопрос должен служить своего рода мотивацией того, почему все Результаты должны иметь возможность выполнять LimitsPassed произвольно Limits. Я спрашиваю, потому что пытаюсь определить, нужен ли public new bool LimitsPassed(SpecificLimits limits) (чтобы скрыть метод базового класса, но при этом предоставить реализацию)

Wyck 14.08.2024 16:09

@Wyck, я отредактировал свой вопрос и добавил некоторую реализацию классов. Я создал классыResults и Limits, потому что я хотел бы вызвать метод LimitsPassed с параметром базового класса и позволить программе решить, какой именно производный тип является точным во время выполнения. Я также пробовал использовать методы расширения, но не могу вызвать их и в базовом классе.

oleq 14.08.2024 16:22

Этот вопрос похож на: Можно ли переопределить метод производным параметром вместо базового?. Если вы считаете, что это другое, отредактируйте вопрос, поясните, чем он отличается и/или как ответы на этот вопрос не помогают решить вашу проблему.

Ivan Petrov 14.08.2024 16:29

Подумайте или расскажите нам, как вы хотите это использовать. Если ваш код окружает List<Result>, вы, вероятно, в основном будете знать, что это Result, и вам будет трудно использовать более конкретную версию базового метода без постоянного знания/проверки/приведения к производному классу Result.

Ralf 14.08.2024 16:41
Стоит ли изучать 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
7
92
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

У вас есть несколько вариантов. Вероятно, вы хотите указать, что все производные классы, реализующие LimitsPassed, используют параметр, производный от класса Limits.

Пример:

    public abstract class Result
    {
        public abstract bool LimitsPassed<T>(T limits) where T:Limits;
    }

    public class SpecificResult : Result
    {
        public override bool LimitsPassed<T>(T limits)
        {
            throw new NotImplementedException();
        }
    }

    public abstract class Limits
    {

    }

    public class SpecificLimits : Limits
    {

    }

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

Пример:

    public abstract class Result
    {
        public abstract bool LimitsPassed(ILimits limits);
    }

    public class SpecificResult : Result
    {
        public override bool LimitsPassed(ILimits limits)
        {
            //specify parameter base type to particular type of derived class
        }
    }

    public interface ILimits
    {

    }

    public class SpecificLimits : ILimits
    {

    }

Во втором примере не имеет значения, является ли это интерфейсом. Вы могли бы оставить это как class Limits вместо interface ILimits. Важным аспектом вашего второго примера является то, что вы изменили тип аргумента на LimitsPassed, чтобы принять тот же тип аргумента, что и метод базового класса.

Wyck 14.08.2024 16:13

@Вик Правда. Однако, поскольку предпочтительнее кодировать интерфейс, а не конкретизировать/реализовать, здесь показан интерфейс.

majixin 14.08.2024 17:20

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

public class SpecificResult : Result
{
    public override bool LimitsPassed(Limits limits)
        => limits is SpecificLimits specificLimits ?
           LimitsPassed(specificLimits) :
           throw new ArgumentException(); // or false

    public bool LimitsPassed(SpecificLimits limits) 
    {
        //specify parameter base type to particular type of derived class
    }
}

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

public class SpecificResult : Result
{
    public override bool LimitsPassed(Limits limits) {
        if (limits is SpecificLimits specificLimits) {
            // Do something with specificLimits
        } else {
            // TODO: Handle case where Limits provided are not SpecificLimits
            throw new ArgumentException("Limits must be of type SpecificLimits");
        }
    }
}
Ответ принят как подходящий

Судя по комментариям и вашему вопросу, я не вижу ничего общего с Limits, так что, возможно, у нас может быть базовый Result класс, который будет выглядеть так (я сделал LimitsPassed return null вместо того, чтобы генерировать исключения для недопустимых типов пределов, но это вопрос выбора конечно)

public abstract class Result {
    // can be bool but then we have to throw exceptions
    // when invalid Limits type
    public abstract bool? LimitsPassed(object limits);

    // helps us when we have List<Result>
    // so we can filter the results that support the limit
    // check for certain Limit Types
    public abstract Type[] AllowedLimitTypes { get; }
}

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

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

public abstract class Result<TLimit> : Result {
    Type[] _types = new Type[] { typeof(TLimit) };
    public override Type[] AllowedLimitTypes => _types;

    // default implementation so we don't have to do it every time in our derived classes
    // sealed override doesn't allow derived classes to break the contract 
    // of having just a single limit as generic parameter 
    // (for example by including checks for multiple limits types in this method)
    // those classes should derive from the non-generic Result instead
    public sealed override bool? LimitsPassed(object limits) {
        if (limits is TLimit tLimit) return LimitsPassed(tLimit);
        return null;
    }
    public abstract bool LimitsPassed(TLimit limits);
}

public class SpecificResult : Result<SpecificLimits> {
    public double MeasureA { get; set; }
    public string MeasureB { get; set; }

    public override bool LimitsPassed(SpecificLimits limits) {
        return this.MeasureA >= limits.MeasureA_min
                && this.MeasureA <= limits.MeasureA_max
                && this.MeasureB == limits.MeasureB_master;
    }
}

Для нескольких типов лимитов у нас есть немного больше работы:

public class SpecificResult2 : Result {

    Type[] _types = new Type[] { typeof(SpecificLimits), typeof(SpecificLimits2) };
    public override Type[] AllowedLimitTypes => _types;

    public override bool? LimitsPassed(object limits) {
        return limits switch {
            SpecificLimits specLimits => LimitsPassed(specLimits),
            SpecificLimits2 specLimits2 => LimitsPassed(specLimits2),
            _ => null
        };
    }

    public bool LimitsPassed(SpecificLimits limits) {
        // implementation
        return true;
    }

    public bool LimitsPassed(SpecificLimits2 limits) {
        // implementation
        return true;
    }
}

У нас также могут быть общие удобные классы для более распространенных случаев — два или три TLimits:

public abstract class Result<TLimit1,TLimit2> 
public abstract class Result<TLimit1,TLimit2,TLimit3> 

Есть ли способ, который не позволяет использовать несколько типов Limits в LimitsPassed без создания общего класса? Я спрашиваю, потому что мой дизайн предполагает, что класс SpecificResult всегда будет ссылаться на SpecificLimits. Например, SpecificResultA всегда будет относиться к SpecificLimitsA, SpecificResultB к SpecificLimitsB и так далее. Мне просто интересно, верна ли моя конструкция. Для меня лучшим подходом без создания общего класса является ответ @Wyck, однако он не защищает меня от передачи несвязанного класса SpecificLimits в метод LimitsPassed. Спасибо.

oleq 16.08.2024 09:52

@oleq общий Result<TLimit> - это скорее удобный класс, который инкапсулирует логику приведения (из ответа @Wyck). В то же время у вас есть то, что вам действительно нужно — более производный тип, принимающий LimitsPassed. Types[] позволяет легко фильтровать List<Result> и применять более производные функции...

Ivan Petrov 16.08.2024 09:59

также с помощью общего класса вы можете легко фильтровать список результатов, т. е. is Result<SpecificLimits>

Ivan Petrov 16.08.2024 10:00

это также дает вам возможность использовать любые объекты (включая структуры) для Liimts-подобных объектов, поэтому вам не придется выполнять там бессмысленное наследование, как это было прокомментировано под вашим вопросом.

Ivan Petrov 16.08.2024 10:06

спасибо за ответ. Я проанализировал ваш подход и считаю, что это лучшая реализация в моем случае.

oleq 16.08.2024 10:19

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