空接口 vs 属性,那泛型约束呢?

14

我有一个类,它使用空接口作为“标记接口”,如下:

namespace MyNameSpace
{
    public interface IMessage
    {
        //nothing in common here...
    }
    public class MyMessage : IMessage
    { 
        public void SendMyMessage()
        {
           //Do something here
        }
    }
}

我在其他帖子和MSDN上读到,(http://msdn.microsoft.com/en-us/library/ms182128.aspx) 这种方式应该被避免使用,而是应该使用自定义属性代替空接口。因此,我可以像这样重构我的代码:

namespace MyNameSpace
{
    public class MessageAttribute : Attribute
    {
         //nothing in common here...
    }
    [MessageAttribute]
    public class MyMessage
    { 
        public void SendMyMessage()
        {
           //Do something here
        }
    }
}

一切都正常工作,但主要问题是:
当我在程序的其他地方有一个通用方法时,例如:
public IEnumerable<T> GetAll<T>() where T : IMessage
{
    //Return all IMessage things here
}

在上面的函数中,我确实需要在 T 上添加一些通用类型约束,以便只允许 IMessages。如果使用自定义属性而不是空接口,我该如何实现这一点呢?
这是否证明了使用空接口的必要性?或者我应该使用一个空的抽象类 Message(而不是接口 IMessage),因为 MyMessage 实际上是一种 Message
非常想知道你对此的看法。

1
你正在尝试用同一件事情来完成两件事情,这可能不是一个好主意。如果你需要限制你的泛型参数类型,那么IMessage不仅仅是一个标记接口。 - Hossain Muctadir
1
@Muctadir:除了一个没有任何成员的接口,仅存在于类型检测(例如通过通用约束)的唯一目的之外,标记接口还有什么? - O. R. Mapper
1
@lazyberezovsky SendMessage只是一个例子,不应该被解释为可能的共享功能。重点是我需要有一些东西来定义我的泛型类型约束在GetAll<T>()中... - Jeroen1984
3
为什么需要类型参数?为什么不能使用GetAll而是要用GetAll<T> where T:IMessageIMessage与方法的逻辑有何相关性?由于IMessage没有成员,所以这个限制有什么作用? - dcastro
1
@O.R.Mapper,我需要这个是因为我可能有几个被标记为消息的类,但它们没有共享的行为或功能。但是我确实需要一个GetAll<T>()函数,其中T:IMessage,它在某种通用存储库类中,我只想返回类型为IMessage的对象集合,比如MyMessage对象,或者也可能是实现IMessage的SomeOtherMessage对象。 - Jeroen1984
显示剩余10条评论
4个回答

8
当使用自定义属性而不是空接口时,我该如何完成这个任务?但是这是否证明了使用空接口的必要性呢?
你无法在编译时完成此操作。当然,你可以在运行时检查属性。
或者我应该使用一个空抽象类Message?
基类比接口更加限制,个人建议采用最少的开销。但我想知道即使是人造的接口(什么也不提供)本身是否就是开销,另一种选择可能只是:不要添加任何这样的要求。让人们添加接口只是为了方法编译并没有给你太多的好处,与其一开始就有一个没有约束的通用方法不如这样做。

4
拥有或不拥有这个接口并不是使它“合理”的原因;带有该接口的东西同样可能不合理,而没有该接口的东西可能是完全合理的。我不知道什么是“合理”的规则,但这与该接口没有任何关系。 - Marc Gravell
3
得到编译错误总比得到运行时错误好,是吧?否则,你可以将所有内容键入 System.Object 并在注意到对象类型不正确时引发 InvalidOperationExceptionNotSupportedException - O. R. Mapper
2
@O.R.Mapper 这里的编译器错误代表着“什么都没有”。用一个空接口让错误消失,但这可能会导致一切崩溃。 - Gusdor
2
@Gusdor:就像我说的那样-我们可以将每个参数都输入为System.Object,完全不需要进行任何编译时类型检查。一些异常仍然可能在运行时发生这个事实,并不意味着我们不应该尽可能地在编译时捕获尽可能多的错误。 - O. R. Mapper
2
@Gusdor:意图不总是通过标识符表达吗?语言确实可以提供一些有限的支持,以检查开发人员的意图是否得到遵守,从而避免错误。例如,具有相同成员的两个接口仍然不是赋值兼容的,因为它们是两种不同的类型。它们在纯语言层面上可能看起来相同,但它们是用不同的意图编写的,被不同的意图使用,并且将作为实现接口的类的不同意图列出。 - O. R. Mapper
显示剩余13条评论

6

由于C#目前没有提供基于属性的泛型约束, 所以您别无选择,只能使用标记接口。

有趣的是,关于CA1040的文档页面规定了以下规则的例外:

当接口用于在编译时标识一组类型时,可以安全地抑制此规则的警告。

在评估泛型约束时,标识“一组类型在编译时”似乎正是所需的,我现在想知道文档页面的作者是否考虑到了这一点。


1
谢谢,我甚至没有阅读那个异常。我会将您的答案标记为采纳的答案,因为这个异常恰好描述了我的情况和我需要使用这个空接口的需求。 - Jeroen1984
我目前正在一個項目中進行同樣的辯論,我同意@MarcGravell的觀點。標記界面可以提供編譯時約束,這聽起來很不錯,但它不一定提供可維護的代碼。如果沒有任何實際的接口方法,您將不得不在運行時進行類型檢查/轉換,以便使用任何IMessage實例。如果需要添加任何新的IMessage對象,您必須跟踪到每個IMessage的使用情況,以確保處理了新類型。 - Scott
@Scott:“你被迫在运行时进行类型检查/转换,以便使用任何IMessage实例” - 如果IMessage对象仅用作包含将通过反射处理(例如序列化)的任意数据的容器,则不完全正确。“如果需要添加任何新的IMessage对象,则必须跟踪每个IMessage的使用情况,以确保处理新类型。” - 这与标记接口无关,而是发生在您使用基本接口但想要使用子类型引入的成员的任何情况下。 - O. R. Mapper

2
在Microsoft的文档中,一些建议说“通常比Y更喜欢X”,但更有帮助的是指出在某些情况下每种方法都是正确的,而另一种方法则完全错误。
关于属性、标记接口和组合接口的选择,它们各自的语义将表明何时适用或不适用。在特定情况下,选择哪种语义可能是一个判断调用,但如果需要特定的语义,则一般来说选择属性和接口之间不是一个判断调用。
任何实现公共接口的类都会代表自己和其后代向整个世界作出承诺,即对该类的任何对象的引用都将引用实现该接口的东西。这样做,一个类将使任何派生类无法避免作出同样的承诺。相比之下,具有承诺某些特性的属性的未封闭类仅承诺该特性将应用于该类的实例。不能保证它将适用于由基类类型的引用标识的派生类实例。
如果想要指定某些特性将适用于所有派生类型,则应使用某种形式的接口来表示该特性。如果希望允许派生类单独决定是否广告基类广告的特征,则应将特征表示为属性。
请注意,即使决定使用接口来表示特性,也不意味着应使用空标记接口。如果某个特性只有在与还实现其他接口的对象一起使用时才有用,则继承自一个或多个其他接口的组合接口可能比必须将其与其他接口组合在一起的标记接口更有用。 among other things, 如果有一个空标记接口例如IIsImmutable和一个或多个类包括ImmutableList < T>,它们都实现了IIsImmutable和例如IEnumerable < T>,则可以传递任何类型的引用,该类型实现了IIsImmutable和IEnumerable < T>到参数被限制为IIsImmutable和IEnumerable < T>的方法,但是给定一个Object,其类型未知,除了可以转换为两种接口类型之外,没有类型可以安全地将这样的对象转换为满足两个约束条件的类型。如果没有使用标记接口,而是定义了接口IImmutableEnumerable < out T>:IEnumerable < T>,那么实现IEnumerable < T>并希望广告其不变性的对象可以转换为IImmutableEnumerable < T>。
在某些情况下,拥有一个接口ISelf<out T> { T Value {get;}}可能会很有用,然后让标记接口接受泛型类型参数T并继承ISelf<T>。需要一个不可变实现的IEnumerable<T>的代码可以使用IImmutable<IEnumerable<T>>类型的参数。对于任何实现ISelf<其自身类型>的类对象的引用都可以自由地转换为它实现的任何组合的标记接口。这种方法的唯一困难在于,将类型为IImmutable<IEnumerable<T>>thing用作IEnumerable<T>需要使用例如thing.Value.GetEnumerator(),而虽然可能期望thing.Valuething应该是同一个对象(暗示thing.Value应该实现thing所具有的所有接口),但类型系统中没有强制执行这一点。

0

当接口包含SendMyMessage方法时,这并不是错误的,因为该方法对于该接口是必需的。

 namespace MyNameSpace
 {
    public interface IMessage
    {
       // If your architecture need this method and this method belong to this interface then why not ?!
       SendMyMessage();
    }

    public class MyMessage : IMessage
    { 
       public void SendMyMessage()
        {
          //Do something here
        }
    }
}

例如,您可以这样做:
public class MessageRepository<T> where T : IMessage
{
  public IEnumerable<T> GetAll()
  {
    throw new NotImplementedException();
  }
}

或者:

public class MessageRepository
  {
    public IEnumerable<T> GetAll<T>() where T : IMessage
    {
      throw new  NotImplementedException();
    }
  }

2
谢谢您的回答,但SendMessage只是一个例子,不应被解释为可能的共享功能。关键是我需要有一些东西来定义我的通用类型约束在GetAll<T>()中... - Jeroen1984
你也可以为存储库创建一个接口,这是你要寻找的吗? - Bassam Alugili

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接