58

之前在 Stack Overflow 上讨论过,我们应该更喜欢属性而不是标记接口(没有任何成员的接口)。MSDN 上的界面设计文章也提出了这个建议:

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

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

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

避免空接口

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

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

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

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

真正的问题来了:微软在框架类库的设计中没有遵循他们自己的建议(至少在几个情况下):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 %>

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

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

4

6 回答 6

16

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

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

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

interface MyInterface<T> {}

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

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

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

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

interface MyMixin {}

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

class MyClass : MyMixin {
}

循环访问者模式也使用它们。有时也使用术语“退化界面”。

更新:

我不知道这是否重要,但我已经使用它们来标记类以供后编译器处理。

于 2010-08-07T23:10:39.907 回答
7

微软在做 .NET 1.0 的时候并没有严格遵循这些指导方针,因为指导方针是随着框架一起进化的,而有些规则他们没有学会,直到改变 API 为时已晚。

IIRC,您提到的示例属于 BCL 1.0,因此可以对其进行解释。

这在框架设计指南中进行了解释。


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

它接着说,有时您需要标记接口来进行编译时检查,而这对于属性来说是不可能的。但是,我发现书中 (p. 88) 中给出的示例无法令人信服,因此在此不再赘述。

于 2010-01-18T14:07:52.293 回答
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 |

但这并不是显着差异。

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>();
        }
    }
}
于 2018-05-11T08:34:21.420 回答
4

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

于 2010-01-18T17:55:05.897 回答
3

从编码的角度来看,我认为我更喜欢标记接口语法,因为内置关键字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);            
    }
} 
于 2014-07-26T12:17:07.773 回答