Неопровержимые причины использовать интерфейсы маркеров вместо атрибутов - PullRequest
54 голосов
/ 18 января 2010

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 100 * * * 100% * (не имеет членов). Статья по дизайну интерфейса на MSDN также подтверждает эту рекомендацию:

Избегайте использования маркерных интерфейсов (интерфейсов без элементов).

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

Существует даже правило FxCop для обеспечения выполнения этой рекомендации:

Избегайте пустых интерфейсов

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

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

В статье указана только одна причина, по которой вы можете игнорировать предупреждение: когда вам нужно идентифицировать время компиляции для типов. (Это соответствует статье «Дизайн интерфейса»).

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

Здесь возникает актуальный вопрос: Microsoft не выполнила свою собственную рекомендацию при разработке библиотеки классов Framework (по крайней мере, в нескольких случаях): Интерфейс IRequiresSessionState и Интерфейс IReadOnlySessionState . Эти интерфейсы используются платформой ASP.NET для проверки того, должна ли она включать состояние сеанса для определенного обработчика или нет. Очевидно, он не используется для идентификации типов во время компиляции. Почему они этого не сделали? Я могу думать о двух возможных причинах:

  1. Микрооптимизация: проверка, реализует ли объект интерфейс (obj is IReadOnlySessionState), быстрее, чем использование отражения для проверки атрибута (type.IsDefined(typeof(SessionStateAttribute), true)). В большинстве случаев разница незначительна, но может иметь значение для пути к критичному для производительности коду во время выполнения ASP.NET. Однако есть обходные пути, которые они могли бы использовать, например, кэширование результата для каждого типа обработчика. Интересно, что Web-сервисы ASMX (которые имеют схожие характеристики производительности) фактически используют для этой цели свойство EnableSession атрибута WebMethod .

  2. Реализация интерфейсов потенциально более вероятна, чем украшение типов с атрибутами сторонними языками .NET. Поскольку ASP.NET разработан, чтобы быть независимым от языка, и ASP.NET генерирует код для типов (возможно, на стороннем языке с помощью CodeDom ), которые реализуют указанные интерфейсы на основе EnableSessionState атрибут директивы <%@ Page %> , возможно, имеет смысл использовать интерфейсы вместо атрибутов.

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

Это просто (преждевременная?) Оптимизация или маленькая ошибка в дизайне фреймворка? (Они думают, что отражение - это "большой монстр с красными глазами" ?) Мысли?

Ответы [ 6 ]

14 голосов
/ 01 февраля 2010

Я обычно избегаю "маркерных интерфейсов", потому что они не позволяют снять отметку производного типа Но кроме этого, вот некоторые из конкретных случаев, которые я видел, где интерфейсы маркеров предпочтительнее встроенной поддержки метаданных:

  1. Чувствительные к производительности ситуации во время выполнения.
  2. Совместимость с языками, которые не поддерживают аннотации или атрибуты.
  3. Любой контекст, в котором заинтересованный код может не иметь доступа к метаданным.
  4. Поддержка общих ограничений и общей дисперсии (обычно для коллекций).
10 голосов
/ 08 августа 2010

Для универсального типа вы можете использовать тот же универсальный параметр в интерфейсе маркера. Это не достижимо атрибутом:

interface MyInterface<T> {}

class MyClass<T, U> : MyInterface<U> {}

class OtherClass<T, U> : MyInterface<IDictionary<U, T>> {}

Этот тип интерфейса может быть полезен для связи типа с другим.

Еще одно хорошее применение для интерфейса маркера - это когда вы хотите создать вид миксина :

interface MyMixin {}

static class MyMixinMethods {
  public static void Method(this MyMixin self) {}
}

class MyClass : MyMixin {
}

Ациклический шаблон также использует их. Иногда используется также термин «вырожденный интерфейс».

UPDATE:

Я не знаю, считается ли это, но я использовал их, чтобы пометить классы для посткомпилятора для работы.

6 голосов
/ 18 января 2010

Microsoft не строго следовала руководящим указаниям при создании .NET 1.0, потому что руководящие принципы развивались вместе со структурой и некоторыми правилами, которые они не изучали, пока не стало слишком поздно менять API.

IIRC, упомянутые вами примеры относятся к BCL 1.0, что объясняет это.

Это объясняется в Руководстве по проектированию платформы .


ЭтоКроме того, в книге также отмечается, что «тестирование [A] ttribute намного дороже, чем проверка типов» (на боковой панели Рико Мариани).

Далее говорится, что иногда вам нужен интерфейс маркера дляпроверка времени компиляции, что невозможно с атрибутом.Тем не менее, я нахожу пример, приведенный в книге (стр. 88), неубедительным, поэтому я не буду повторять его здесь.

4 голосов
/ 18 января 2010

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

3 голосов
/ 26 июля 2014

С точки зрения кодирования, я думаю, что я предпочитаю синтаксис интерфейса маркера из-за встроенных ключевых слов as и is. Маркировка атрибута требует немного больше кода.

[MarkedByAttribute]
public class MarkedClass : IMarkByInterface
{
}

public class MarkedByAttributeAttribute : Attribute
{
}

public interface IMarkByInterface
{
}

public static class AttributeExtension
{
    public static bool HasAttibute<T>(this object obj)
    {
        var hasAttribute = Attribute.GetCustomAttribute(obj.GetType(), typeof(T));
        return hasAttribute != null;
    }
}

И некоторые тесты для использования кода:

using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class ClassMarkingTests
{
    private MarkedClass _markedClass;

    [TestInitialize]
    public void Init()
    {
        _markedClass = new MarkedClass();
    }

    [TestMethod]
    public void TestClassAttributeMarking()
    {
        var hasMarkerAttribute = _markedClass.HasAttibute<MarkedByAttributeAttribute>();
        Assert.IsTrue(hasMarkerAttribute);
    }

    [TestMethod]
    public void TestClassInterfaceMarking()
    {
        var hasMarkerInterface = _markedClass as IMarkByInterface;
        Assert.IsTrue(hasMarkerInterface != null);            
    }
} 
2 голосов
/ 11 мая 2018

С точки зрения производительности:

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

Это применимо только при использовании его в часто вызываемом коде.

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i5-2400 CPU 3.10GHz (Sandy Bridge), 1 CPU, 4 logical and 4 physical cores
Frequency=3020482 Hz, Resolution=331.0730 ns, Timer=TSC
.NET Core SDK=2.1.300-rc1-008673
  [Host] : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
  Core   : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT

Job=Core  Runtime=Core

                     Method |          Mean |      Error |     StdDev | Rank |
--------------------------- |--------------:|-----------:|-----------:|-----:|
                     CastIs |     0.0000 ns |  0.0000 ns |  0.0000 ns |    1 |
                     CastAs |     0.0039 ns |  0.0059 ns |  0.0052 ns |    2 |
            CustomAttribute | 2,466.7302 ns | 18.5357 ns | 17.3383 ns |    4 |
 CustomAttributeWithCaching |    25.2832 ns |  0.5055 ns |  0.4729 ns |    3 |

Это не значительная разница, хотя.

namespace BenchmarkStuff
{
    [AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
    public class CustomAttribute : Attribute
    {

    }

    public interface ITest
    {

    }

    [Custom]
    public class Test : ITest
    {

    }

    [CoreJob]
    [RPlotExporter, RankColumn]
    public class CastVsCustomAttributes
    {
        private Test testObj;
        private Dictionary<Type, bool> hasCustomAttr;

        [GlobalSetup]
        public void Setup()
        {
            testObj = new Test();
            hasCustomAttr = new Dictionary<Type, bool>();
        }

        [Benchmark]
        public void CastIs()
        {
            if (testObj is ITest)
            {

            }
        }

        [Benchmark]
        public void CastAs()
        {
            var itest = testObj as ITest;
            if (itest != null)
            {

            }
        }

        [Benchmark]
        public void CustomAttribute()
        {
            var customAttribute = (CustomAttribute)testObj.GetType().GetCustomAttributes(typeof(CustomAttribute), false).SingleOrDefault();
            if (customAttribute != null)
            {

            }
        }

        [Benchmark]
        public void CustomAttributeWithCaching()
        {
            var type = testObj.GetType();
            bool hasAttr = false;
            if (!hasCustomAttr.TryGetValue(type, out hasAttr))
            {
                hasCustomAttr[type] = type.CustomAttributes.SingleOrDefault(attr => attr.AttributeType == typeof(CustomAttribute)) != null;
            }
            if (hasAttr)
            {

            }
        }
    }

    public static class Program
    {
        public static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<CastVsCustomAttributes>();
        }
    }
}
...