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

Я работал над этим примером:

Классы питания:

public class Food { }

public class Meat : Food { }

public class Grass : Food { }

Классы животных:

public abstract class Animal<TFood>
    where TFood : Food
{
    public void Feed(TFood food) { }
}

public abstract class Carnivore : Animal<Meat> { }

public abstract class Herbivore : Animal<Grass> { }

public class Cow : Herbivore { }

Классы фермы:

public abstract class Farm<TAnimal, TFood>
    where TAnimal : Animal<TFood>
    where TFood : Food
{
    public List<TAnimal> Animals;

    public TFood FoodSupply;

    public void FeedAnimals()
    {
        foreach ( var animal in Animals )
        {
            animal.Feed(FoodSupply);
        }
    }
}

public class DariyFarm : Farm<Cow, Grass> { }

И меня раздражало, что для молочной фермы мне приходилось определять тип корма, так как тип корма уже должна определять корова.

Я чувствую, что здесь что-то упускаю. В идеале я хотел бы иметь возможность определять ферму только с точки зрения того, какое животное она выращивает, и тогда тип пищи будет определяться животным. К сожалению, вы не можете применить ограничение Animal к U, не указав тип еды T.

Что мне не хватает?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать 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
0
230
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

К сожалению, именно так работает общая система типов в C#.

У вас есть эта проблема, потому что:

  • Класс Animal имеет метод Feed(T food). Это ключ.
  • Класс Farm имеет T FoodSupply, чтобы кормить внутри него Animal.
  • Класс Farm должен вызывать Feed(T food) на Animal, но он не может этого сделать, не зная, что такое T.

Вы можете обойти это с помощью интерфейсов. Скажем, у вас был интерфейс IAnimal:

public interface IAnimal
{
    void Feed(Food food);
}

тогда Animal<T> мог бы реализовать это:

public abstract class Animal<TFood> : IAnimal where TFood : Food
{
    public void Feed(TFood food)
    {
        // we either need to check for null here
    }

    public void Feed(Food food)
    {
        // or we need to check food is TFood here
        Feed(food as TFood);
    }
}

Затем вы можете изменить свой класс Farm, чтобы полностью избавиться от дженериков:

public abstract class Farm<TAnimal> where TAnimal : IAnimal
{
    public List<TAnimal> Animals;

    public Food FoodSupply;

    public void FeedAnimals()
    {
        foreach ( var animal in Animals )
        {
            animal.Feed(FoodSupply);
        }
    }
}

Теперь проблема в том, что у вас может быть DairyFarm (скажем), для которого FoodSupply является Meat, но вы не можете скормить MeatCow, поскольку они едят только Grass.

Вы необходимость должны иметь оба аргумента типа для использования дженериков — компилятор не может вывести конкретный тип Food из Animal<T>.

And I found it annoying that for a dairy farm I had to define the type of food, since the type of food should already be defined by the cow.

Это ваша логическая ошибка прямо здесь; система типов С# понятия не имеет, что «ферма», «корова» и «еда» имеют эти отношения в вашем уме. Ваша мысль такова: «коровы едят корм, поэтому ферма должна быть автоматически параметризована кормом», но фермы также производить корма; как компилятор узнает, что вы намерены логически связать «еду» с едой, которую едят коровы, а не с едой, произведенной на ферме?

What am I missing?

Вы пытаетесь внедрить бизнес-логику в свою систему типов. Не делай этого; как вы обнаружили, это создает проблемы.

Я написал серию статей в блоге о некоторых из многих способов сделать это неправильно, и вы сами уже на этом пути. Возможно, вам будет интересно почитать: https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/

Я не думаю, что это просто бизнес-логика, класс Cow имеет внутри себя тип Plant, я просто хочу, чтобы был способ получить к нему доступ. В любом случае, спасибо за замечания, я прочитаю эту статью.

pseudoabdul 22.01.2019 07:31

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