У меня прога на C# и есть съедаемые объекты (растения и травоядные) и едящие объекты(травоядные и хищники). Хочу сделать такую схему классов:
// я съедаемый животным, у которого жертва я сам
public interface IEatableBy<TEater>
where TEater : Animal<IEatableBy<TEater>>
{
void BeEatenBy(TEater animal);
}
public abstract class Animal<TVictim>
//моя жертва съедаема мной самим
where TVictim : IEatableBy<Animal<TVictim>>
{
/// <summary>
/// жертвы
/// </summary>
protected HashSet<TVictim> Victims;
}
public class Plant : IEatableBy<Herbivore>
{
public void BeEatenBy(Herbivore animal)
{
}
}
public class Predator : Animal<Herbivore>
{ }
public class Herbivore : Animal<Plant>, IEatableBy<Predator>
{
public void BeEatenBy(Predator animal)
{
}
}
Проблема в том, что при такой схеме компилер выдает такие ошибки:
1) на строке
public interface IEatableBy<TEater>
подчеркивает TEater и пишет: Тип "Assets.IEatableBy<TEater>" не может быть использован как параметр типа "TVictim" в универсальном типе или методе "Animal<TVictim>". Нет преобразования неявной ссылки из "Assets.IEatableBy<TEater>" в "Assets.IEatableBy<Animal<Assets.IEatableBy<TEater>>>"
2) на строке
public abstract class Animal<TVictim> : MonoBehaviour
подчеркивает TVictim и пишет: Тип "Animal<TVictim>" не может быть использован как параметр типа "TEater" в универсальном типе или методе "IEatableBy<TEater>". Нет преобразования неявной ссылки из "Animal<TVictim>" в "Animal<Assets.IEatableBy<Animal<TVictim>>>"
Мои вопросы:
1) Почему появляются такие ошибки при компиляции? Моя схема со всеми ее констрейнтами нарушает какой-то здравый смысл или логику (тогда каким образом оно нарушается)?
2) Если моя схема не нарушает здравый смысл, то почему не компилится? Недостаток компилера сишарпа? То есть теоретически в будущем она может начать компилиться?
3) Как правильно реализовать этот мой замысел, но чтобы оно компилилось?
На третий вопрос многие ответят: "зачем делать эту лапшу и генериков и констрейнтов, ничего в ней не понятно. Избавься от них и все заработает". Но почему я тут хочу использовать именно генерики и именно такие ограничения? Очевидно, чтобы Хищники могли есть только травоядных, а травоядные только растения. И чтобы в производном классе "Травоядные" проперти
protected HashSet<TVictim> Victims;
содержало только растения и метод
public void BeEatenBy(Predator animal)
при оверрайде принимал параметром только хищников. И по аналогии со всеми другими классами.
Здравствуйте, gamburger, Вы писали:
G>Моя схема со всеми ее констрейнтами нарушает какой-то здравый смысл или логику (тогда каким образом оно нарушается)?
Похоже, нарушается ко-/контравариантность. Погугли про ключевые слова in/out в generic'ах в контексте covariance/contravariance, в каком направлении меняется вариантность входных и выходных параметров функций.
Здравствуйте, Qbit86, Вы писали:
Q>Здравствуйте, gamburger, Вы писали:
G>>Моя схема со всеми ее констрейнтами нарушает какой-то здравый смысл или логику (тогда каким образом оно нарушается)?
Q>Похоже, нарушается ко-/контравариантность. Погугли про ключевые слова in/out в generic'ах в контексте covariance/contravariance, в каком направлении меняется вариантность входных и выходных параметров функций.
Я уже пробовал делать параметр интерфейса ковариантным или контравариантным.
public interface IEatableBy<in TEater>
и
public interface IEatableBy<in TEater>
В случае с in ничего не меняется, в случае с out ошибка с TVictim остается, но ошибка с TEater пропадает (зато появляется другая ошибка, что в методе BeEatenBy параметр TEater не может быть входящим, но с этим все понятно).
Если рассмотреть схему на примере на примере Хищника и Тревоядного, то:
1)
public class Predator: Animal<Herbivore>
следовательно
2)
public abstract class Animal<Herbivore>
//моя жертва съедаема мной самим
where Herbivore: IEatableBy<Animal<Herbivore>>
следовательно
3)
public interface IEatableBy<Animal<Herbivore>>
where Animal<Herbivore>: Animal<IEatableBy<Animal<Herbivore>>>
идем обратно в анимал:
4)
public abstract class Animal<IEatableBy<Animal<Herbivore>>>
//моя жертва съедаема мной самим
where IEatableBy<Animal<Herbivore>>: IEatableBy<Animal<IEatableBy<Animal<Herbivore>>>>
потом идем дальше обратно в IEatableBy и т.д.
То есть тут видно, что 2) отличается от 4) , но мой мозг слишком слаб, чтобы самому понять, что именно тут не так. Но я хочу это наконец понять, т.к. уже не первый раз попадаю в такую лапшу с генериками. Может, кто-нить поймет, что там происходит, из-за чего не компилится, и что можно сделать, чтобы получиться компилящийся код, при этом чтобы хищники не могли есть растения.
Тут же хитрованская рекурсия и я не уверен, что компилятор может с ней справиться.
Здесь есть обсуждение похожего случая с ссылкой на блог Липперта, где он разбирает похожий случай.
А зачем вообще городить такой сложный дизайн классов?
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, gamburger, Вы писали:
S>Тут же хитрованская рекурсия и я не уверен, что компилятор может с ней справиться.
Если бы дело было только в рекурсии, то компилер должен был бы писать про рекурсию. Но он конкретно пишет про то, что что-то куда-то не приводится.
S>Здесь есть обсуждение похожего случая с ссылкой на блог Липперта, где он разбирает похожий случай.
Про кота, который может дружить с evilDog, я читал (но в моем случае, вроде как, не совсем та ситуация, там по ссылке хотят для котов ограничить параметр типа только котами, но я же не хочу никак ограничить параметр типа TVictim для Хищников, я хочу, чтобы TVictim мог быть любым, но чтобы при этом проперти Victims было строго типизировано типом TVictim).
S>А зачем вообще городить такой сложный дизайн классов?
Дело не в том, что я пытаюсь максимально усложнить прогу без получения каких-либо преимуществ, а в том, что я в первую очередь хочу получить преимущества (что список жертв Хицника всегда будет состоять только из Травоядных, а список жертв Травоядного всегда будет состоять из Растений, а также BeEatableBy у Травоядного всегда будет вызываться только с Хищником, а у Растения всегда только с Травоядным, то есть я хочу все строго типизировать), и в результате попытки все строго типизировать у меня получается всегда лапша с генериками.
Я думаю, что должно быть возможно задать такие настройки генериков и констрейнтов, чтобы все строго типизировалось и при этом компилилось, если только сишарп сам не является преградой, потому что с точки зрения логики я не вижу тут проблем.
Либо, может, кто-то предложит другую схему без таких констрейнтов, но чтобы все строго типизировалось.
G>Я думаю, что должно быть возможно задать такие настройки генериков и констрейнтов, чтобы все строго типизировалось и при этом компилилось, если только сишарп сам не является преградой, потому что с точки зрения логики я не вижу тут проблем.
Именно с тз логики и проблема, у Вас по сути рекурсивное определение -- абстрактный класс зависит от интерфейса, интерфейс зависит от асбтрактного класса.
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, gamburger, Вы писали:
G>>Я думаю, что должно быть возможно задать такие настройки генериков и констрейнтов, чтобы все строго типизировалось и при этом компилилось, если только сишарп сам не является преградой, потому что с точки зрения логики я не вижу тут проблем.
S>Именно с тз логики и проблема, у Вас по сути рекурсивное определение -- абстрактный класс зависит от интерфейса, интерфейс зависит от асбтрактного класса.
В случае циклич. зависимости
class Q1 : Q2
{ }
class Q2 : Q1
{ }
компилер пишет: Циклическая зависимость базового класса включает "Q2" и "Q1"
А в моем случае он пишет совсем не то.
Но даже если предположить, что проблема именно в рекурсии, то тогда как реализовать схему для такой задачи?
Есть Хищник, Травоядное, Растение.
Хищник и Травоядное должны иметь список жертв, т.к. проперти общее, то его выносим в базовый класс Животное, и нужно, чтобы у Хищника жертвами могли быть только Травоядные, а у Травоядных жертвами могли быть только Растения.
Травоядные и Растения могут быть одинаково съедены соответвенно Хищниками и Травоядными, т.е. нужен общий метод BeEatenBy(Animal animal). При этом чтобы у Травоядного этот метод был таким BeEatenBy(Хищник animal), а у Растения таким BeEatenBy(Травоядное animal) .
Как сделать такую схему?
S> public interface IEatableBy<TEater>
S> where TEater : Entity
S> {
S> void BeEatenBy(TEater animal);
S> }
S> public class Entity
S> {
S> }
S> public class Animal : Entity { }
S> public abstract class Animal<TVictim>
S> //моя жертва съедаема мной самим
S> where TVictim : IEatableBy<Entity>
S> {
S> /// <summary>
S> /// жертвы
S> /// </summary>
S> protected HashSet<TVictim> Victims;
S> }
S>Либо убрать ограничение у интерфейса, что логичнее. Пожирается кем-то или чем-то, ну и ладно.
Оба случая не подходят под ограничение, что растения должны поедаться только травоядными. Если убрать ограничение у интерфейса, то можно задать, чтобы растение съедалось растениями. Если использовать код выше, то классы должны быть такими
public class Herbivore : Animal<Plant>, IEatableBy<Entity>
{ }
public class Predator : Animal<Herbivore>
{ }
public class Plant : IEatableBy<Entity>
{ }
то есть Herbivore и Plant оба реализуют IEatableBy<Entity> , то есть их съедает Entity, но Entity — это любое животное, а нужно, чтобы травоядное съедалось только хищниками, а растения только травоядными.
S>Либо убрать ограничение у интерфейса, что логичнее. Пожирается кем-то или чем-то, ну и ладно.
Неужели на практике так всегда и делается? Но если не будет нормальной типизации, то я же могу ошибиться и передать в Растение в качестве едящего Хищника, в результате программа не будет работать, как нужно. В общем, как-то странно, неужели си шарп настолько плох, что там нельзя никак типизировать данные в этом случае?
Здравствуйте, gamburger, Вы писали:
G>У меня прога на C# и есть съедаемые объекты (растения и травоядные) и едящие объекты(травоядные и хищники). Хочу сделать такую схему классов:
На хаскеле вам писать уже советовали?
Такие рекурсивные генерики вы не сделаете в C#. Для рекурсии нужна какая-то явная её остановка — и как вы её зададите в C#?
Здравствуйте, gamburger, Вы писали:
G>У меня прога на C# и есть съедаемые объекты (растения и травоядные) и едящие объекты(травоядные и хищники). Хочу сделать такую схему классов:
Не понял чем "простой" подход не устраивает ("может есть"):
public interface CanEat<T>
{
void eat(T something);
}
public class Plant
{
}
public class Herbivore : CanEat<Plant>
{
public void eat(Plant thing)
{
}
}
public class Predator : CanEat<Herbivore>
{
public void eat(Herbivore thing)
{
}
}
Ну или, наоборот ("может быть съеден")
public interface CanBeEatenBy<T>
{
void beEatenBy(T something);
}
public class Predator
{
}
public class Herbivore : CanBeEatenBy<Predator>
{
public void beEatenBy(Predator something)
{
}
}
public class Plant: CanBeEatenBy<Herbivore>
{
public void beEatenBy(Herbivore something)
{
}
}