Создание ковариантных универсальных типов без нарушения правил пустого интерфейса

Предпосылки: я хотел «расширить» тип .NET Lazy<> для поддержки неявного преобразования между Lazy<T> и нижележащим объектом T, чтобы иметь возможность автоматически разворачивать содержащееся значение. Мне это удалось довольно легко:

public class ExtendedLazy<T> : Lazy<T>
{
    public ExtendedLazy() : base() {}
    public ExtendedLazy(bool isThreadSafe) : base(isThreadSafe) { }
    public ExtendedLazy(Func<T> valueFactory) : base(valueFactory) { }
    // other constructors

    public static implicit operator T(ExtendedLazy<T> obj)
    {
        return obj.Value;
    }
}

Я хотел пойти дальше, сделав T ковариантным, чтобы можно было назначить экземпляр ExtendedLazy<Derived> ExtendedLazy<Base>. Поскольку модификаторы дисперсии не допускаются в определениях классов, для этого мне пришлось прибегнуть к пустому интерфейсу:

public interface IExtendedLazy<out T>
{
}

И изменил определение моего класса на

public class ExtendedLazy<T> : Lazy<T>, IExtendedLazy<T>

Это отлично работает, и я смог использовать этот ковариантный тип:

ExtendedLazy<DerivedClass> derivedLazy = new ExtendedLazy<DerivedClass>();
IExtendedLazy<BaseClass> baseLazy = derivedLazy;

Хотя это компилируется и работает нормально, оно противоречит CA1040: избегайте пустых интерфейсов в котором говорится, что использование пустых интерфейсов в качестве контрактов - это плохой дизайн и неприятный запах кода (и я уверен, что большинство людей с этим согласны). Мой вопрос: учитывая неспособность среды CLR распознавать вариативные универсальные типы в определениях классов, какие еще способы сделать это более согласованным с приемлемыми практиками объектно-ориентированного программирования? Я полагаю, что я не единственный, кто сталкивается с этой проблемой, поэтому я надеюсь получить некоторое представление об этом.


person PoweredByOrange    schedule 01.10.2015    source источник
comment
CA1040 - плохое правило ИМХО. Он сообщает вам использовать атрибуты вместо интерфейсов маркеров, за исключением того, что такая проверка выполняется на несколько порядков медленнее во время выполнения. В вашем случае вам придется добавить к нему T Value { get; } участника в любом случае.   -  person Lucas Trzesniewski    schedule 02.10.2015
comment
@LucasTrzesniewski: если вы кэшируете результаты, тогда разница между интерфейсами маркеров и атрибутами является размытой для большинства случаев использования, поскольку вы обычно используете относительно небольшое количество типов по сравнению с количеством вызовов. CA1040, безусловно, является одной из более слабых и более сомнительных рекомендаций, хотя многие согласятся, что для небольших проектов маркеры менее гибкие, но достаточные для таких случаев.   -  person Guvante    schedule 02.10.2015
comment
@Guvante, конечно, но зачем заставлять меня использовать что-то вроде static ConcurrentDictionary<Type, bool> вместе с вызовами отражения, когда я могу добиться того же с помощью оператора is в одном выражении? И это все еще быстрее, чем поиск в кеше.   -  person Lucas Trzesniewski    schedule 02.10.2015
comment
@LucasTrzesniewski: CA1040 говорит, что если у вас нет метода, у вас нет настоящего интерфейса, у вас есть интерфейс маркера, а интерфейсы маркера - не лучшая идея. Честно говоря, чаще правильно, чем неправильно, что интерфейсы маркеров не являются правильным решением проблемы. Есть проблемы, для которых это хорошее решение, и я согласился, что в таких случаях CA1040 можно игнорировать. Однако большинство вещей, которые хотят использовать интерфейс маркера, хотят использовать несколько интерфейсов маркера, и его быстро становится очень трудно поддерживать, поэтому они не рекомендуют его.   -  person Guvante    schedule 02.10.2015
comment
@Guvante: Да, я согласен, что в большинстве случаев лучше использовать атрибуты. Думаю, я просто не люблю правила в первую очередь :)   -  person Lucas Trzesniewski    schedule 02.10.2015


Ответы (1)


Ваша логика не будет работать так хорошо, как вы думаете.

ExtendedLazy<DerivedClass> derivedLazy = new ExtendedLazy<DerivedClass>();
IExtendedLazy<BaseClass> baseLazy = derivedLazy;
BaseClass v = baseLazy;

Это не будет компилироваться, поскольку не существует преобразования из IExtendedLazy<BaseClass> в BaseClass, поскольку оператор преобразования определен только для ExtendedLazy<T>.

Это заставит вас делать что-то еще при использовании интерфейса. Добавление T Value { get; } решает как проблему CA1040, так и дает вам доступ к базовому значению.

Кстати, причина того, что Lazy<T> не предоставляет implicit operator T, заключается в том, что базовый Func<T> может вызывать, что может сбивать с толку, поскольку строка, которая выбрасывает, вполне может не иметь вызова функции (или свойства) на нем.

person Guvante    schedule 01.10.2015
comment
Спасибо за ответ. Я понимаю вашу точку зрения, однако я все еще думаю, что могут быть и другие случаи, когда интерфейс не обязательно должен иметь какие-либо члены, но он просто используется исключительно как объявитель вариантного типа. Мы также используем Lazy в очень специфическом сценарии модульного тестирования, так что здесь нет никаких опасений. Но ваша точка зрения об отсутствии неявного оператора по умолчанию имеет смысл. - person PoweredByOrange; 02.10.2015