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

Posted

技术标签:

【中文标题】使用标记接口而不是属性的令人信服的理由【英文标题】:Compelling Reasons to Use Marker Interfaces Instead of Attributes 【发布时间】:2011-01-06 09:15:08 【问题描述】:

discussed before on Stack Overflow 我们应该更喜欢属性而不是marker interfaces(没有任何成员的接口)。 Interface Design article on MSDN 也提出了这个建议:

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

自定义属性提供了一种标记类型的方法。有关自定义属性的更多信息,请参阅编写自定义属性。当您可以将属性检查推迟到代码执行时,自定义属性是首选。如果您的方案需要编译时检查,则不能遵守此指南。

甚至还有 FxCop rule 来强制执行此建议:

避免空接口

接口定义了提供行为或使用契约的成员。接口描述的功能可以被任何类型采用,无论该类型出现在继承层次结构中的什么位置。类型通过为接口的成员提供实现来实现接口。空接口没有定义任何成员,因此也没有定义可以实现的合约。

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

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

如果接口用于在编译时识别一组类型,则可以安全地从该规则中排除警告。

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

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

    第三方 .NET 语言可能比使用属性装饰类型更可能支持实现接口。由于 ASP.NET 被设计为与语言无关,并且 ASP.NET 为基于 @ 的 EnableSessionState 属性实现上述接口的类型(可能在 CodeDom 的帮助下使用第三方语言)生成代码987654330@,使用接口而不是属性可能更有意义。

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

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

【问题讨论】:

冒着听起来太尖刻的风险,微软做过什么让你期望他们能做到一致性?多年来,他们提供了许多有用的工具和软件,但我从未从他们身上看到的一件事是一致的行为,或遵守他们自己的准则或规则。 @jalf:我同意他们因不遵守自己的准则而臭名昭著,但公平地说,他们在 .NET 方面做得很好。 .NET Framework 表现出大量的一致性,并且通常设计得非常好。 一般设计得非常好,是的,而且异常一致,但并不完美。回想起来,有很多愚蠢的小设计错误是显而易见的。但正如 Mark 的回答所说,这些问题在设计 BCL 时可能并不那么明显——因此指南建议也不是。 @jalf:有时设计早于指南推荐。 【参考方案1】:

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

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

【讨论】:

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

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

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 学习。

【讨论】:

能够在没有任何接口成员的情况下实现 MyMixin 的任何类型上有效实现 Mixin 的机会非常低。 我不明白为什么关系的“持久性”与标记界面是否适合它有任何关系。你会在哪里使用 IFood 接口而不是更合适的属性? @David Nelson:我同意,这与使用属性并没有什么不同。但是mixin的用法很有趣,我没有得到你的评论。 @DavidNelson:一个对象可以做的事情并不完全由它的成员决定。事实上,有时对象承诺做某事可能很有用。尽管微软在命名上未能区分可读、只读和不可变的事物,但如果将它们标准化,识别后两者的标记接口可能会有所帮助。不可变对象的内容可以通过封装对不可变对象的引用来封装,并且对合法只读对象的引用可以安全地暴露给代码...... ...不能相信它会避免修改可变对象。如果存在一种标准方法来确定哪些对象可以以这种方式安全使用或公开,那么能够直接使用或公开对象而不必复制其内容或将它们封装在只读包装器中将很有帮助。不幸的是,不存在标准的识别方法。【参考方案3】:

微软在制作 .NET 1.0 时并没有严格遵循这些准则,因为这些准则是随着框架一起演变的,而其中一些规则他们直到更改 API 为时已晚才学习。

IIRC,你提到的例子属于BCL 1.0,这样就可以解释了。

Framework Design Guidelines 对此进行了解释。


也就是说,这本书还提到“[A] 属性测试比类型检查成本高得多”(在 Rico Mariani 的侧边栏中)。

接着说,有时您需要标记接口来进行编译时检查,而这对于属性来说是不可能的。但是,我觉得书中(第 88 页)中给出的例子没有说服力,所以我不会在这里重复。

【讨论】:

是的,他们经常违反自己的准则,但通常有其背后的原因。问题是,为什么他们不遵循这个特定的指导方针?有什么真正的优势还是仅仅是设计错误? 我一直认为这只是一个错误。我现在没有这本书,但是 IIRC 它专门讨论了他们使用标记接口的示例,因为“他们不知道更好”... 既然我有机会在书中查找它,我已经在我的答案中添加了更多信息。 是的,性能是我提到的第一点(微优化)。我不相信它,因为 ASMX 使用它并且可以缓存它。编译时检查是使用接口的一个令人信服的理由(如指南和 FxCop 规则中所述),但它不适用于这种特定情况。他们有没有写过他们说这是一个错误的地方? 好吧,我没有重读整本书,但我查了索引指向的页面,他们没有说任何关于那个区域的错误。我可能把我记忆中的例子弄混了。【参考方案4】:

从性能的角度来看:

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

【讨论】:

【参考方案5】:

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

【讨论】:

您能否详细说明您的回答中为什么会这样想?我的意思是为什么你认为属性和异常不应该“影响正常的处理逻辑”? (您所说的“正常处理逻辑”是什么意思?异常确实会影响程序的流程,而且不仅仅适用于调试器。) 好的,我会尝试详细说明一下。首先,.NET 属性(或 Java 注释)是一项相对较新的发明,我认为 UML 中甚至没有针对它们的标准表示。我知道它们现在被广泛用于 AOP 和后处理(PostSharp 等人),但这只是试图弥补编程语言的某些功能缺乏或过多的限制。 “正常处理逻辑”是指应用程序或系统中的常规“控制流”,而不是“异常”。当他们想要返回不适合他们选择的简单返回数据类型的东西时,有些人会抛出疯狂的异常,而他们最好应该对事物进行不同的建模。这与属性类似。以属性元数据的 [MaxLength(x)] 属性为例,或者 [DefaultValue("foo")] 是我的特别朋友,因为他不可本地化。 这些东西属于专用的类型元数据类而不是属性。如果我想通过配置文件或数据库初始化这些东西怎么办?我不得不摆弄 TypeDescriptor 和 PropertyDescriptor 的东西才能让事情正常工作,而且感觉很脏。总而言之,几乎所有可以用 Attributes 表达的东西也可以用不同的方式建模,无论是通过更好的支持类还是对配置的支持。例如,对于后期处理,我会发现标记接口方法更清洁。 再一次,任何可以用类完成的事情都可以用不同的方式建模。关键是它们可以使表达某些内容变得更加容易。【参考方案6】:

从编码的角度来看,我认为我更喜欢标记接口语法,因为内置关键字asis。属性标记需要更多代码。

[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);            
    
 

【讨论】:

以上是关于使用标记接口而不是属性的令人信服的理由的主要内容,如果未能解决你的问题,请参考以下文章

检查方法是不是实现了标有属性的接口方法

如何在 WCF 中将接口标记为 DataContract

返回类型为标记接口时使用 Jackson 进行多态序列化

什么是Java Marker Interface(标记接口)

什么是Java Marker Interface(标记接口)

什么是Java Marker Interface(标记接口)