Подтвердить что ты не робот

Почему не типичные ограничения типа, наследуемые/иерархически принудительные

Класс предмета

public class Item
{
    public bool Check(int value) { ... }
}

Базовый абстрактный класс с общим типом ограничений

public abstract class ClassBase<TItem>
    where TItem : Item
{
    protected IList<TItem> items;

    public ClassBase(IEnumerable<TItem> items)
    {
        this.items = items.ToList();
    }    

    public abstract bool CheckAll(int value);
}

Унаследованный класс без ограничений

public class MyClass<TItem> : ClassBase<TItem>
{
    public override bool CheckAll(int value)
    {
        bool result = true;
        foreach(TItem item in this.items)
        {
            if (!item.Check(value)) // this doesn't work
            {
                result = false;
                break;
            }
        }
        return result;
    }
}

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

Я делаю что-то неправильно, понимая это неправильно или действительно ли это ограничение общего типа не наследуется? Если последнее верно, , почему в мире это?

Немного дополнительного объяснения

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

Предположим, что мы имеем все три класса, как указано выше. Тогда мы также имеем этот класс:

public class DanteItem
{
    public string ConvertHellLevel(int value) { ... }
}

Как мы видим, этот класс не наследует от Item, поэтому он не может использоваться как конкретный класс как ClassBase<DanteItem> (забудьте о том, что ClassBase является абстрактным на данный момент. обычный класс). Поскольку MyClass не определяет каких-либо ограничений для своего типового типа, кажется совершенно справедливым, что MyClass<DanteItem>...

Но. Вот почему я считаю, что ограничения общего типа должны быть унаследованы/применены в унаследованных классах так же, как и для ограничений типа generic типа, потому что если мы посмотрим на определение MyClass, то он говорит:

MyClass<T> : ClassBase<T>

Когда T есть DanteItem, мы можем видеть, что он автоматически не может использоваться с MyClass, потому что он унаследован от ClassBase<T> и DanteItem не выполняет свое ограничение общего типа. Я мог бы сказать, что ** общий тип на MyClass зависит от ограничений типа ClassBase общего типа, поскольку в противном случае MyClass может быть создан с любым типом. Но мы знаем, что этого не может быть.

Было бы, конечно, иначе, если бы я имел MyClass, определяемый как:

public class MyClass<T> : ClassBase<Item>

в этом случае T не имеет ничего общего с базовым классом, поэтому он не зависит от него.

Это все немного объяснение/рассуждение. Я мог бы просто подвести итог:

Если мы не предоставляем ограничение общего типа на MyClass, это подразумевает, что мы можем создать экземпляр MyClass с любым конкретным типом. Но мы знаем, что это невозможно, так как MyClass наследуется от ClassBase и что у него есть ограничение общего типа.

Надеюсь, теперь это имеет гораздо больше смысла.

4b9b3361

Ответ 1

ДРУГОЕ ОБНОВЛЕНИЕ:

Этот вопрос был тема моего блога в июле 2013 года. Спасибо за отличный вопрос!

ОБНОВЛЕНИЕ:

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

Некоторые упрощенные примеры:

class B<T> where T:C {}
class D<U> : B<U> {}

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

Как насчет этого?

class B<T, U> where T : X where U : Y {}
class D<V> : B<V, V> {}

Теперь V - это параметр типа, используемый в контексте, где он должен быть как X, так и Y. Поэтому, по вашему мнению, компилятор должен вывести это и автоматически поставить ограничение X и Y на V. Да?

Как насчет этого?

class B<T> where T : C<T> {}
class C<U> : B<D<U>> where U : IY<C<U>> {}
class D<V> : C<B<V>> where V : IZ<V> {}

Я только что сделал это, но заверяю вас, что это совершенно законная иерархия. Просьба описать четкое и последовательное правило, которое не входит в бесконечные циклы для определения того, что все ограничения находятся на T, U и V. Не забывайте обрабатывать случаи, когда параметры типа известны как ссылочные типы, а ограничения интерфейса имеют ковариантные или контравариантные аннотации! Кроме того, алгоритм должен обладать свойством, что он дает точно такие же результаты независимо от того, какой порядок B, C и D отображаются в исходном коде.

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

Что такого особенного в базовых типах? Почему бы не реализовать эту функцию до конца?

class B<T> where T : X {}
class D<V> { B<V> bv; }

V - параметр типа, используемый в контексте, где он должен быть конвертируемым в X; поэтому компилятор должен вывести этот факт и поставить ограничение X на V. Да? Или нет?

Почему поля специальные? Что об этом:

class B<T> { static public void M<U>(ref U u) where U : T {} }
class D<V> : B<int> { static V v; static public void Q() { M(ref v); } }

V - это параметр типа, используемый в контексте, где он может быть только int. Поэтому компилятор С# должен вывести этот факт и автоматически поместить ограничение int на V.

Да? Нет?

Вы видите, где это происходит? Где это останавливается? Чтобы правильно реализовать желаемую функцию, компилятор должен выполнить анализ всей программы.

Компилятор не выполняет этот уровень анализа, потому что это ставит телегу перед лошадью. Когда вы создаете общий файл, вам необходимо доказать компилятору, что вы удовлетворили ограничение. Это не задача компилятора, чтобы понять, что вы хотели сказать, и определить, какой дополнительный набор ограничений удовлетворяет первоначальному ограничению.

По аналогичным причинам компилятор также не пытается автоматически выводить аннотации вариаций в интерфейсах от вашего имени. Подробнее см. В моей статье на эту тему.

http://blogs.msdn.com/b/ericlippert/archive/2007/10/29/covariance-and-contravariance-in-c-part-seven-why-do-we-need-a-syntax-at-all.aspx


Оригинальный ответ:

Я хотел бы знать, почему наследования не являются типичными ограничениями типа?

Наследуются только члены. Ограничение не является членом.

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

Вы просто утверждаете, как что-то должно быть, не давая объяснений, почему так должно быть. Объясните нам, почему вы верите, что мир должен быть таким; каковы преимущества и каковы недостатки и каковы издержки?

Я делаю что-то неправильно, понимая это неправильно или действительно ли это ограничение общего типа не наследуется?

Общие ограничения не наследуются.

Если последнее верно, почему в мире это?

По умолчанию функции "не реализованы". Нам не нужно указывать причину, почему функция не реализована! Каждая функция не выполняется, пока кто-то не тратит деньги на ее реализацию.

Теперь я спешу заметить, что общие типы ограничений наследуются на методы. Методы являются членами, члены наследуются, и ограничение является частью метода (хотя и не является частью его подписи). Таким образом, ограничение возникает вместе с методом, когда оно унаследовано. Когда вы говорите:

class B<T> 
{
    public virtual void M<U>() where U : T {}
}

class D<V> : B<IEnumerable<V>>
{
    public override void M<U>() {}
}

Тогда D<V>.M<U> наследует ограничение и заменяет IEnumerable<V> для T; таким образом, ограничение состоит в том, что U должен быть конвертируемым в IEnumerable<V>. Обратите внимание, что С# не позволяет вам переустановить ограничение. Это, на мой взгляд, неправильная работа; Я хотел бы иметь возможность пересчитать ограничение для ясности.

Но D не наследует никаких ограничений на T из B; Я не понимаю, как это возможно. M является членом B и наследуется D вместе со своим ограничением. Но T не является членом B в первую очередь, так что же наследует?

Я действительно не понимаю, какая особенность вам нужна. Можете ли вы объяснить более подробно?

Ответ 2

Я думаю, вы сбиты с толку, потому что заявляете, что вы создали класс с помощью TItem.

Если вы думаете об этом, если используете вместо этого Q.

public class MyClass<Q> : BaseClass<Q>
{
 ...
}

Тогда как определить, что Q имеет тип item?

Вам нужно также добавить ограничение к производным классам Generic Type, поэтому

public class MyClass<Q> : BaseClass<Q> were Q : Item { ... } 

Ответ 3

Ниже приведен сценарий, в котором неявный характер этого поведения вызывает различное поведение, чем ожидалось:

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

В этом примере есть класс Operator, который реализует два аналогичных интерфейса: IMonitor и IProcessor. Оба имеют метод запуска и свойство IsStarted, но поведение для каждого интерфейса в классе Operator является отдельным. То есть в классе Operator есть переменная _MonitorStarted и переменная _ProcessorStarted.

MyClass<T> происходит от ClassBase<T>. ClassBase имеет ограничение типа на T, которое должно реализовать интерфейс IProcessor, и согласно предложенному поведению MyClass наследует это ограничение типа.

MyClass<T> имеет метод проверки, который построен с предположением, что он может получить значение свойства IProcessor.IsStarted из внутреннего объекта IProcessor.

Предположим, что кто-то изменил реализацию ClassBase, чтобы удалить ограничение типа IProcessor на общий параметр T и заменить его типом contraint IMonitor. Этот код будет работать бесшумно, но приведет к другому поведению. Причина в том, что метод Check в MyClass<T> теперь вызывает свойство IMonitor.IsStarted вместо свойства IProcessor.IsStarted, хотя код для MyClass<T> не изменился вообще.

public interface IMonitor
{
    void Start();

    bool IsStarted { get; }
}

public interface IProcessor
{
    void Start();

    bool IsStarted { get; }
}

public class Operator : IMonitor, IProcessor
{
    #region IMonitor Members

    bool _MonitorStarted;

    void IMonitor.Start()
    {
        Console.WriteLine("IMonitor.Start");
        _MonitorStarted = true;
    }

    bool IMonitor.IsStarted
    {
        get { return _MonitorStarted; }
    }

    #endregion

    #region IProcessor Members

    bool _ProcessorStarted;

    void IProcessor.Start()
    {
        Console.WriteLine("IProcessor.Start");
        _ProcessorStarted = true;
    }

    bool IProcessor.IsStarted
    {
        get { return _ProcessorStarted; }
    }

    #endregion
}

public class ClassBase<T>
    where T : IProcessor
{
    protected T Inner { get; private set; }

    public ClassBase(T inner)
    {
        this.Inner = inner;
    }

    public void Start()
    {
        this.Inner.Start();
    }
}

public class MyClass<T> : ClassBase<T>
    //where T : IProcessor
{
    public MyClass(T inner) : base(inner) { }

    public bool Check()
    {
        // this code was written assuming that it is calling IProcessor.IsStarted
        return this.Inner.IsStarted;
    }
}

public static class Extensions
{
    public static void StartMonitoring(this IMonitor monitor)
    {
        monitor.Start();
    }

    public static void StartProcessing(this IProcessor processor)
    {
        processor.Start();
    }

}

class Program
{
    static void Main(string[] args)
    {
        var @operator = new Operator();

        @operator.StartMonitoring();

        var myClass = new MyClass<Operator>(@operator);

        var result = myClass.Check();

        // the value of result will be false if the type constraint on T in ClassBase<T> is where T : IProcessor
        // the value of result will be true if the type constraint on T in ClassBase<T> is where T : IMonitor
    }
}

Ответ 4

Поскольку ClassBase имеет ограничение на свой шаблон (должно быть по типу Item), вы должны также добавить это ограничение в MyClass. Если вы этого не сделаете, вы можете создать новый экземпляр MyClass, где шаблон не является типом элемента. При создании базового класса он будет терпеть неудачу.

[править] Хм теперь перечитал ваш вопрос, и я вижу, что ваш код компилируется? Хорошо.

Ну, im MyClass, вы не знаете базовый тип this.items, поэтому вы не можете вызвать метод проверки. this.items имеет тип IList, а в вашем классе TItem не указан, поэтому класс не понимает метод проверки.

Позвольте мне ответить на ваш вопрос, почему бы вам не добавить ограничение в свой класс MyClass? Учитывая любой другой тип класса в качестве шаблона для этого класса, это приведет к ошибке. Почему бы не предотвратить эти ошибки, добавив ограничение, чтобы он не выполнял компиляцию.