Круговые ссылки в генериках
От: gamburger  
Дата: 02.02.18 14:34
Оценка:
У меня прога на 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)

при оверрайде принимал параметром только хищников. И по аналогии со всеми другими классами.
Re: Вариантность
От: Qbit86 Кипр
Дата: 02.02.18 14:46
Оценка:
Здравствуйте, gamburger, Вы писали:

G>Моя схема со всеми ее констрейнтами нарушает какой-то здравый смысл или логику (тогда каким образом оно нарушается)?


Похоже, нарушается ко-/контравариантность. Погугли про ключевые слова in/out в generic'ах в контексте covariance/contravariance, в каком направлении меняется вариантность входных и выходных параметров функций.
Глаза у меня добрые, но рубашка — смирительная!
Re[2]: Вариантность
От: gamburger  
Дата: 02.02.18 15:11
Оценка:
Здравствуйте, 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) , но мой мозг слишком слаб, чтобы самому понять, что именно тут не так. Но я хочу это наконец понять, т.к. уже не первый раз попадаю в такую лапшу с генериками. Может, кто-нить поймет, что там происходит, из-за чего не компилится, и что можно сделать, чтобы получиться компилящийся код, при этом чтобы хищники не могли есть растения.
Отредактировано 02.02.2018 16:22 gamburger . Предыдущая версия .
Re: Круговые ссылки в генериках
От: Sharov Россия  
Дата: 02.02.18 17:33
Оценка: 1 (1) +1
Здравствуйте, gamburger, Вы писали:

Тут же хитрованская рекурсия и я не уверен, что компилятор может с ней справиться.

Здесь есть обсуждение похожего случая с ссылкой на блог Липперта, где он разбирает похожий случай.
А зачем вообще городить такой сложный дизайн классов?
Кодом людям нужно помогать!
Re[2]: Круговые ссылки в генериках
От: gamburger  
Дата: 02.02.18 18:06
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Здравствуйте, gamburger, Вы писали:


S>Тут же хитрованская рекурсия и я не уверен, что компилятор может с ней справиться.


Если бы дело было только в рекурсии, то компилер должен был бы писать про рекурсию. Но он конкретно пишет про то, что что-то куда-то не приводится.

S>Здесь есть обсуждение похожего случая с ссылкой на блог Липперта, где он разбирает похожий случай.


Про кота, который может дружить с evilDog, я читал (но в моем случае, вроде как, не совсем та ситуация, там по ссылке хотят для котов ограничить параметр типа только котами, но я же не хочу никак ограничить параметр типа TVictim для Хищников, я хочу, чтобы TVictim мог быть любым, но чтобы при этом проперти Victims было строго типизировано типом TVictim).

S>А зачем вообще городить такой сложный дизайн классов?


Дело не в том, что я пытаюсь максимально усложнить прогу без получения каких-либо преимуществ, а в том, что я в первую очередь хочу получить преимущества (что список жертв Хицника всегда будет состоять только из Травоядных, а список жертв Травоядного всегда будет состоять из Растений, а также BeEatableBy у Травоядного всегда будет вызываться только с Хищником, а у Растения всегда только с Травоядным, то есть я хочу все строго типизировать), и в результате попытки все строго типизировать у меня получается всегда лапша с генериками.
Я думаю, что должно быть возможно задать такие настройки генериков и констрейнтов, чтобы все строго типизировалось и при этом компилилось, если только сишарп сам не является преградой, потому что с точки зрения логики я не вижу тут проблем.
Либо, может, кто-то предложит другую схему без таких констрейнтов, но чтобы все строго типизировалось.
Re[3]: Круговые ссылки в генериках
От: Sharov Россия  
Дата: 02.02.18 18:30
Оценка:
Здравствуйте, gamburger, Вы писали:


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


Именно с тз логики и проблема, у Вас по сути рекурсивное определение -- абстрактный класс зависит от интерфейса, интерфейс зависит от асбтрактного класса.
Кодом людям нужно помогать!
Re[4]: Круговые ссылки в генериках
От: gamburger  
Дата: 02.02.18 19:16
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Здравствуйте, gamburger, Вы писали:



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


S>Именно с тз логики и проблема, у Вас по сути рекурсивное определение -- абстрактный класс зависит от интерфейса, интерфейс зависит от асбтрактного класса.


В случае циклич. зависимости
class Q1 : Q2
{ }
class Q2 : Q1
{ }

компилер пишет:
Циклическая зависимость базового класса включает "Q2" и "Q1"

А в моем случае он пишет совсем не то.
Но даже если предположить, что проблема именно в рекурсии, то тогда как реализовать схему для такой задачи?
Есть Хищник, Травоядное, Растение.
Хищник и Травоядное должны иметь список жертв, т.к. проперти общее, то его выносим в базовый класс Животное, и нужно, чтобы у Хищника жертвами могли быть только Травоядные, а у Травоядных жертвами могли быть только Растения.
Травоядные и Растения могут быть одинаково съедены соответвенно Хищниками и Травоядными, т.е. нужен общий метод BeEatenBy(Animal animal). При этом чтобы у Травоядного этот метод был таким BeEatenBy(Хищник animal), а у Растения таким BeEatenBy(Травоядное animal) .
Как сделать такую схему?
Re[5]: Круговые ссылки в генериках
От: Sharov Россия  
Дата: 02.02.18 19:36
Оценка:
Здравствуйте, gamburger, Вы писали:


G>В случае циклич. зависимости

G>
G>class Q1 : Q2
G>{ }
G>class Q2 : Q1
G>{ }
G>

G>компилер пишет:
G>Циклическая зависимость базового класса включает "Q2" и "Q1"

G>А в моем случае он пишет совсем не то.


По сути тоже самое, но другими словами.

G>Как сделать такую схему?


public interface IEatableBy<TEater>
where TEater : Entity
{
void BeEatenBy(TEater animal);
}


public class Entity
{

}
public class Animal : Entity { }

public abstract class Animal<TVictim>
//моя жертва съедаема мной самим
where TVictim : IEatableBy<Entity>
{
/// <summary>
/// жертвы
/// </summary>
protected HashSet<TVictim> Victims;
}


Либо убрать ограничение у интерфейса, что логичнее. Пожирается кем-то или чем-то, ну и ладно.
Кодом людям нужно помогать!
Re[6]: Круговые ссылки в генериках
От: gamburger  
Дата: 02.02.18 20:49
Оценка:
Здравствуйте, Sharov, Вы писали:

S>

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>Либо убрать ограничение у интерфейса, что логичнее. Пожирается кем-то или чем-то, ну и ладно.


Неужели на практике так всегда и делается? Но если не будет нормальной типизации, то я же могу ошибиться и передать в Растение в качестве едящего Хищника, в результате программа не будет работать, как нужно. В общем, как-то странно, неужели си шарп настолько плох, что там нельзя никак типизировать данные в этом случае?
Re: Круговые ссылки в генериках
От: Слава  
Дата: 02.02.18 21:14
Оценка:
Здравствуйте, gamburger, Вы писали:

G>У меня прога на C# и есть съедаемые объекты (растения и травоядные) и едящие объекты(травоядные и хищники). Хочу сделать такую схему классов:


На хаскеле вам писать уже советовали?

Такие рекурсивные генерики вы не сделаете в C#. Для рекурсии нужна какая-то явная её остановка — и как вы её зададите в C#?
Re: Круговые ссылки в генериках
От: Буравчик Россия  
Дата: 04.02.18 23:58
Оценка: +1
Здравствуйте, 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)
     {
     }
}
Best regards, Буравчик
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.