Пустой интерфейс vs Атрибут, как насчет общих ограничений?

У меня есть класс, который использует пустой интерфейс в качестве "интерфейса маркера", например:

namespace MyNameSpace
{
    public interface IMessage
    {
        //nothing in common here...
    }
    public class MyMessage : IMessage
    { 
        public void SendMyMessage()
        {
           //Do something here
        }
    }
}

Я читаю в некоторых других сообщениях, но также и в MSDN (http://msdn.microsoft.com/en-us/library/ms182128.aspx), чего следует избегать, и вы должны использовать пользовательские атрибуты вместо этого пустого интерфейса. Итак, я мог бы реорганизовать свой код следующим образом:

namespace MyNameSpace
{
    public class MessageAttribute : Attribute
    {
         //nothing in common here...
    }
    [MessageAttribute]
    public class MyMessage
    { 
        public void SendMyMessage()
        {
           //Do something here
        }
    }
}

Все работает отлично, но главный вопрос:

Когда у меня есть общий метод в другом месте моей программы, например:

public IEnumerable<T> GetAll<T>() where T : IMessage
{
    //Return all IMessage things here
}

В вышеприведенной функции мне нужно добавить некоторые общие ограничения типа на T, поэтому разрешены только IMessages. Как я мог выполнить это при использовании настраиваемого атрибута вместо пустого интерфейса?

И оправдывает ли это использование пустого интерфейса? Или я должен использовать пустой абстрактный класс Message (вместо интерфейса IMessage, так как MyMessage на самом деле является Message).

Очень интересно, что вы думаете об этом.

Ответ 1

Как С# в настоящее время не предлагает никаких общих ограничений на основе атрибутов, у вас мало другого выбора, кроме как пойти с интерфейсом маркера.

Интересно, что на странице документации на CA1040 указано следующее правило:

Безопасно пресекать предупреждение из этого правила, когда интерфейс используется для идентификации набора типов во время компиляции.

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

Ответ 2

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

Вы не можете - по крайней мере, не во время компиляции. Разумеется, вы можете проверить атрибут во время выполнения.

Или я должен использовать пустой абстрактный класс Message

Базовые классы более строгие, чем интерфейсы; лично я бы навязывал наименьшие накладные расходы. Но мне кажется, что даже искусственный интерфейс (который ничего не дает) сам по себе просто накладные расходы, а другой вариант будет просто: не ставьте такие требования. Создание людей добавляет интерфейс, так что компиляция метода не дает вам намного больше, чем просто иметь общий метод без ограничения в первую очередь.

Ответ 3

Некоторые рекомендации в документации Microsoft говорят, что "в целом предпочитают X более Y", когда было бы более полезно отметить, что есть случаи, когда каждая из них будет правильной, а другая будет просто неправильной.

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

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

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

Обратите внимание, что даже если вы решите использовать интерфейс для выражения признака, это не означает, что нужно использовать пустой интерфейс маркера. Если характеристика будет полезна только в сочетании с объектами, которые также реализуют какой-либо другой интерфейс, составной интерфейс, который наследуется от одного или нескольких других интерфейсов, может быть намного полезнее, чем интерфейс маркера, который должен сочетаться с другими в общем ограничении. Среди прочего, если у одного был пустой интерфейс маркера, например. IIsImmutable и один или несколько классов, включая ImmutableList<T>, который реализовал как IIsImmutable, так и, например, IEnumerable<T>, можно было бы передать ссылку любого типа, который реализует как IIsImmutable, так и IEnumerable<T> метод, параметр которого был ограничен IIsImmutable и IEnumerable<T>, но с учетом Object, тип которого неизвестен за исключением того, что он применяется для обоих типов интерфейсов, нет никакого типа, к которому такой объект можно было бы безопасно отливать, который бы удовлетворял обеим ограничениям. Если вместо использования интерфейса маркера один определил интерфейс IImmutableEnumerable<out T> : IEnumerable<T>, то объекты, которые реализуют IEnumerable<T> и хотят объявить о своей неизменности, могут быть переведены в IImmutableEnumerable<T>.

В некоторых случаях может оказаться полезным иметь интерфейс ISelf<out T> { T Value {get;}}, а затем использовать интерфейсы маркеров с параметром общего типа T и наследовать от ISelf<T>. Код, который затем нуждается в неизменной реализации IEnumerable<T>, может принимать параметр типа IImmutable<IEnumerable<T>>. Ссылка на любой объект класса, который реализует ISelf<itsOwnType>, может быть свободно добавлена ​​к любой комбинации интерфейсов маркеров, которые она реализует. Единственные трудности с этим подходом состоят в том, что использование thing типа IImmutable<IEnumerable<T>> в качестве IEnumerable<T> потребует использования, например, thing.Value.GetEnumerator(), и, хотя можно ожидать, что thing.Value и thing должны быть одним и тем же объектом (подразумевая, что thing.Value должен реализовывать все интерфейсы, которые thing), ничто в системе типов не обеспечило бы этого.

Ответ 4

Это не так, когда интерфейс содержит SendMyMessage, этот метод необходим для этого интерфейса.

 namespace MyNameSpace
 {
    public interface IMessage
    {
       // If your architecture need this method and this method belong to this interface then why not ?!
       SendMyMessage();
    }

    public class MyMessage : IMessage
    { 
       public void SendMyMessage()
        {
          //Do something here
        }
    }
}

Например, вы можете сделать это:

public class MessageRepository<T> where T : IMessage
{
  public IEnumerable<T> GetAll()
  {
    throw new NotImplementedException();
  }
}

или:

public class MessageRepository
  {
    public IEnumerable<T> GetAll<T>() where T : IMessage
    {
      throw new  NotImplementedException();
    }
  }