代码之家  ›  专栏  ›  技术社区  ›  Mehrdad Afshari

使用标记接口而不是属性的重要原因

  •  54
  • Mehrdad Afshari  · 技术社区  · 15 年前

    一直以来 discussed before on Stack Overflow 我们更喜欢属性 marker interfaces (没有任何成员的接口)。 Interface Design article on MSDN 也申明这项建议:

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

    自定义属性提供了标记类型的方法。有关自定义属性的详细信息,请参见编写自定义属性。当您可以将属性的检查推迟到代码执行时,首选自定义属性。如果您的场景需要编译时检查,则不能遵守此准则。

    甚至还有一个 FxCop rule 执行本建议:

    避免空接口

    接口定义提供行为或使用约定的成员。接口所描述的功能可以被任何类型采用,无论该类型出现在继承层次结构中的何处。类型通过为接口的成员提供实现来实现接口。空接口不定义任何成员,因此也不定义可实现的协定。

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

    本文只说明了一个可能忽略警告的原因:当您需要类型的编译时标识时。(这与接口设计文章一致)。

    如果在编译时使用接口标识一组类型,则从该规则中排除警告是安全的。

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

    1. 微观优化:检查对象是否实现接口( obj is IReadOnlySessionState )比使用反射检查属性快( type.IsDefined(typeof(SessionStateAttribute), true) ). 这种差异在大多数情况下可以忽略不计,但实际上对于ASP.NET运行时中的性能关键代码路径来说可能很重要。但是,他们也可以使用一些变通方法,比如缓存每个处理程序类型的结果。有趣的是,ASMX Web服务(具有类似的性能特征)实际上使用 EnableSession property WebMethod attribute 为此目的。

    2. 实现接口可能比用第三方.NET语言的属性修饰类型更受支持。因为ASP.NET被设计成不依赖语言,并且ASP.NET为类型生成代码(可能在第三方语言的帮助下 CodeDom )基于 EnableSessionState 的属性 <%@ Page %> directive ,使用接口而不是属性可能更有意义。

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

    这仅仅是(过早的)吗优化还是框架设计中的一个小错误?(他们认为 reflection is a "big monster with red eyes" ?) 思想?

    6 回复  |  直到 7 年前
        1
  •  14
  •   LBushkin    15 年前

    我通常避免使用“标记接口”,因为它们不允许您 取消标记 派生类型。但除此之外,我还看到了一些特定的例子,其中标记接口比内置元数据支持更可取:

    1. 运行时性能敏感的情况。
    2. 与不支持注释或属性的语言兼容。
    3. 感兴趣的代码可能无法访问元数据的任何上下文。
    4. 支持泛型约束和泛型差异(通常是集合)。
        2
  •  10
  •   Jordão    12 年前

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

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

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

    标记接口的另一个好用法是 kind of mixin :

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

    这个 acyclic visitor pattern 也使用它们。有时也使用“退化接口”一词。

    更新:

    我不知道这个算不算,但我用它们来标记 post-compiler 继续工作。

        3
  •  6
  •   Mark Seemann    15 年前

    微软在制作.NET 1.0时并没有严格遵循这些指导原则,因为这些指导原则是与框架一起发展的,有些规则他们直到太晚才学会更改API。

    IIRC,你提到的例子属于BCL1.0,所以这可以解释它。

    这在 Framework Design Guidelines .


    也就是说,这本书还评论说“属性测试比类型检查要昂贵得多”(在Rico Mariani的侧边栏中)。

    它接着说,有时您需要标记接口来进行编译时检查,而这对于属性是不可能的。然而,我发现书中给出的例子(第88页)无法令人信服,所以我不会在这里重复。

        4
  •  4
  •   herzmeister    15 年前

    我非常支持标记界面。我从不喜欢属性。我将它们视为类和成员的某种元信息,例如供调试器查看。与异常类似,它们不应该影响正常的处理逻辑,在我看来,这是最起码的。

        5
  •  3
  •   Mikael Engver    10 年前

    从编码的角度来看,我认为我更喜欢标记接口语法,因为内置的关键字 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);            
        }
    } 
    
        6
  •  2
  •   Konrad    6 年前

    从性能角度来看:

    由于反射,标记属性将比标记接口慢。如果不缓存反射,则调用 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>();
            }
        }
    }