使用标记接口而不是属性的令人信服的理由

Meh*_*ari 55 .net asp.net attributes interface marker-interfaces

它已经讨论过堆栈溢出,我们应该更喜欢属性标记接口(接口没有任何成员).MSDN上的接口设计文章也断言了这个建议:

避免使用标记接口(没有成员的接口).

自定义属性提供了标记类型的方法.有关自定义属性的更多信息,请参阅编写自定义属性.如果可以在执行代码之前推迟检查属性,则首选自定义属性.如果您的方案需要编译时检查,则无法遵守此准则.

甚至还有一个FxCop规则来强制执行此建议:

避免空接口

接口定义提供行为或使用合同的成员.无论类型在继承层次结构中出现何种位置,接口描述的功能都可以采用任何类型.类型通过为接口的成员提供实现来实现接口.空接口不定义任何成员,因此,不定义可以实现的合同.

如果您的设计包含期望实现类型的空接口,则可能使用接口作为标记,或者标识一组类型的方法.如果此标识将在运行时发生,则完成此操作的正确方法是使用自定义属性.使用属性的存在或不存在或属性的属性来标识目标类型.如果标识必须在编译时进行,则可以使用空接口.

本文仅说明了您可能忽略警告的一个原因:何时需要对类型进行编译时识别.(这与界面设计文章一致).

如果在编译时使用接口标识一组类型,则可以安全地从此规则中排除警告.

实际问题是:Microsoft在框架类库的设计中(至少在几种情况下)不符合他们自己的建议:IRequiresSessionState接口IReadOnlySessionState接口.ASP.NET框架使用这些接口来检查它是否应该为特定处理程序启用会话状态.显然,它不用于类型的编译时识别.他们为什么不这样做?我可以想到两个潜在的原因:

  1. 微优化:检查对象是否实现接口(obj is IReadOnlySessionState)比使用反射检查属性(type.IsDefined(typeof(SessionStateAttribute), true))更快.大多数时候差异可以忽略不计,但它实际上可能对ASP.NET运行时中的性能关键代码路径很重要.但是,他们可以使用的解决方法就像为每个处理程序类型缓存结果一样.有趣的是,ASMX Web服务(具有类似性能特征)实际上使用EnableSession属性WebMethod属性来实现此目的.

  2. 与使用第三方.NET语言的属性装饰类型相比,可能更有可能支持实现接口.由于ASP.NET被设计为与语言无关,并且ASP.NET 根据指令的属性为类型(可能在CodeDom的帮助下以第三方语言生成)生成代码,因此可能会生成更多感觉使用接口而不是属性.EnableSessionState<%@ Page %>

使用标记接口而不是属性的有说服力的原因是什么?

这只是一个(过早?)优化还是框架设计中的一个小错误?(他们认为反射是"红眼睛的大怪物"吗?)思想?

LBu*_*kin 14

我通常避免使用"标记接口",因为它们不允许您取消标记派生类型.但除此之外,以下是我所看到的一些特定情况,其中标记接口比内置元数据支持更可取:

  1. 运行时性能敏感的情况.
  2. 与不支持注释或属性的语言的兼容性.
  3. 感兴趣的代码可能无法访问元数据的任何上下文.
  4. 支持通用约束和通用方差(通常是集合).

  • 我建议在派生类型中标记接口不能“取消标记”这一事实通常是*使用它们的令人信服的理由。如果传递给方法的对象必须具有通过标记接口定义的某些特征,那么`theMethod&lt;T&gt;(T it) where T:IHasAttribute` 将在编译时强制执行。相比之下,即使有人知道 `Fred` 有一个属性,`theMethod(Fred it)` 可以传递一个没有该属性的对象,在运行时失败。话虽如此,我认为大多数标记式接口应该至少继承一个其他接口。 (2认同)

Jor*_*dão 10

对于泛型类型,您可能希望在标记接口中使用相同的泛型参数.这是属性无法实现的:

interface MyInterface<T> {}

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

class OtherClass<T, U> : MyInterface<IDictionary<U, T>> {}
Run Code Online (Sandbox Code Playgroud)

这种类型的接口可能有助于将类型与另一种类型相关联.

标记界面的另一个好用途是当你想要创建一种mixin时:

interface MyMixin {}

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

class MyClass : MyMixin {
}
Run Code Online (Sandbox Code Playgroud)

非周期访问者模式也使用它们.有时也使用术语"简并界面".

更新:

我不知道这个是否重要,但我已经用它们来标记后期编译器的类.

  • 能够在任何类型上有效实现Mixin的机会非常低,这种类型可以在接口中没有任何成员的情况下实现MyMixin. (2认同)

Mar*_*ann 6

微软在制作.NET 1.0时没有严格遵循这些指导原则,因为指南与框架一起发展,并且一些规则在改变API时为时已晚.

IIRC,你提到的例子属于BCL 1.0,所以这可以解释它.

框架设计指南中对此进行了解释.


也就是说,该书还指出"[A]属性测试比类型检查要昂贵得多"(在Rico Mariani的附文中).

接着说,有时您需要标记接口进行编译时检查,这对于属性是不可能的.但是,我发现书中给出的例子(第88页)并不令人信服,所以我在此不再重复.

  • 我一直认为这只是一个错误.我现在没有这本书,但IIRC专门讨论了他们使用Marker Interfaces的例子,因为"他们不知道更好"...... (2认同)

Kon*_*rad 5

从性能角度来看:

由于反射,标记属性将比标记接口慢。如果不缓存反射,那么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 |
Run Code Online (Sandbox Code Playgroud)

但这并没有显着差异。

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>();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)