当前位置:首页> 正文

关于c#:结构实现接口安全吗?

关于c#:结构实现接口安全吗?

Is it safe for structs to implement interfaces?

我似乎还记得读过一些有关结构如何通过C#在CLR中实现接口的弊端,但是我似乎找不到任何东西。 不好吗 这样做会带来意想不到的后果吗?

1
2
public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }

由于没有其他人明确提供此答案,因此我将添加以下内容:

好。

在结构上实现接口不会产生任何负面影响。

好。

用于保存结构的接口类型的任何变量都将导致使用该结构的框式值。如果该结构是不可变的(一件好事),那么这将是最糟糕的性能问题,除非您是:

好。

  • 将结果对象用于锁定目的(无论如何,都是一个非常糟糕的主意)
  • 使用引用相等语义,并期望它可用于来自同一结构的两个装箱值。
  • 好。

    这两种情况均不太可能,相反,您可能正在执行以下一项操作:

    好。

    泛型

    结构实现接口的许多合理原因也许是,以便可以在具有约束的通用上下文中使用它们。以这种方式使用变量时,如下所示:

    好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Foo< T > : IEquatable<Foo< T >> where T : IEquatable< T >
    {
        private readonly T a;

        public bool Equals(Foo< T > other)
        {
             return this.a.Equals(other.a);
        }
    }
  • 启用将struct用作类型参数

  • 只要不使用其他约束,例如new()class
  • 好。

  • 避免对以这种方式使用的结构进行装箱。
  • 那么this.a不是接口引用,因此不会导致放置任何内容的盒子。此外,当c#编译器编译通用类并需要插入在Type参数T的实例上定义的实例方法的调用时,它可以使用受约束的操作码:

    好。

    If thisType is a value type and thisType implements method then ptr is passed unmodified as the 'this' pointer to a call method instruction, for the implementation of method by thisType.

    Ok.

    这避免了装箱,并且由于值类型正在实现,接口必须实现该方法,因此不会发生装箱。在上面的示例中,Equals()调用是在this.a1上没有任何框的情况下完成的。

    好。

    低摩擦API

    大多数结构应具有类似原始的语义,其中按位相同的值被视为等于2。运行时将在隐式Equals()中提供此类行为,但这可能会很慢。同样,此隐式相等不会作为IEquatable< T >的实现公开,因此,除非结构体自己明确实现它,否则防止将结构轻松用作Dictionary的键。因此,许多公共结构类型通常声明它们实现IEquatable< T >(其中T是它们自身),以使其更容易更好地执行,并与CLR BCL中许多现有值类型的行为保持一致。

    好。

    BCL中的所有原语至少要实现:

    好。

  • IComparable
  • IConvertible
  • IComparable< T >
  • IEquatable< T >(因此是IEquatable)
  • 好。

    许多还实现了IFormattable,此外,许多系统定义的值类型(如DateTime,TimeSpan和Guid)也实现了其中的许多或全部。如果您要实现类似"广泛有用"的类型(例如复数结构或某些固定宽度的文本值),则(正确地)实现许多这些公共接口将使您的结构更有用和可用。

    好。

    排除项目

    显然,如果接口强烈暗示可变性(例如ICollection),则实现它不是一个好主意,因为这意味着您要么使结构可变,要么导致描述了一些错误,而这些错误已在盒装值上进行了修改而不是原始版本),或者通过忽略诸如Add()之类的方法的含义或引发异常而使用户感到困惑。

    好。

    许多接口并不暗示可变性(例如IFormattable),而是作为惯用方式以一致的方式公开某些功能。通常,结构的用户不会理会此类行为的任何装箱费用。

    好。

    摘要

    在不可变的值类型上明智地完成后,实现有用的接口是一个好主意

    好。

    笔记:

    1:请注意,当对已知具有特定结构类型但需要调用虚拟方法的变量调用虚拟方法时,编译器可能会使用此方法。例如:

    好。

    1
    2
    3
    List<int> l = new List<int>();
    foreach(var x in l)
        ;//no-op

    List返回的枚举数是一个结构,是一种优化方法,可以避免在枚举列表时进行分配(具有一些有趣的结果)。但是,foreach的语义指定如果枚举器实现IDisposable,则在迭代完成后将调用Dispose()。显然,通过框式调用发生这种情况将消除枚举数为struct的任何好处(实际上会更糟)。更糟糕的是,如果dispose调用以某种方式修改了枚举器的状态,那么这将在装箱的实例上发生,并且在复杂的情况下可能会引入许多细微的错误。因此,在这种情况下发出的IL为:

    好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    IL_0001:  newobj      System.Collections.Generic.List..ctor
    IL_0006:  stloc.0    
    IL_0007:  nop        
    IL_0008:  ldloc.0    
    IL_0009:  callvirt    System.Collections.Generic.List.GetEnumerator
    IL_000E:  stloc.2    
    IL_000F:  br.s        IL_0019
    IL_0011:  ldloca.s    02
    IL_0013:  call        System.Collections.Generic.List.get_Current
    IL_0018:  stloc.1    
    IL_0019:  ldloca.s    02
    IL_001B:  call        System.Collections.Generic.List.MoveNext
    IL_0020:  stloc.3    
    IL_0021:  ldloc.3    
    IL_0022:  brtrue.s    IL_0011
    IL_0024:  leave.s     IL_0035
    IL_0026:  ldloca.s    02
    IL_0028:  constrained. System.Collections.Generic.List.Enumerator
    IL_002E:  callvirt    System.IDisposable.Dispose
    IL_0033:  nop        
    IL_0034:  endfinally

    因此,IDisposable的实现不会引起任何性能问题,并且如果Dispose方法实际上可以执行任何操作,则枚举器的(可遗憾的)可变方面将得以保留!

    好。

    2:double和float是该规则的例外,其中NaN值不相等。

    好。

    好。


    这个问题有几件事发生...

    结构可以实现接口,但是转换,可变性和性能会引起关注。请参阅此帖子以获取更多详细信息:http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

    通常,结构应用于具有值类型语义的对象。通过在结构上实现接口,您可能会在结构与接口之间来回转换时遇到拳击问题。装箱的结果是,更改结构内部状态的操作可能无法正常运行。


    在某些情况下,结构体可以实现一个接口(如果它从来没有用过,那么.net的创建者是否会为此提供接口是令人怀疑的)。如果一个结构体实现了IEquatable< T >之类的只读接口,将该结构体存储在类型为IEquatable< T >的存储位置(变量,参数,数组元素等)中,则需要将其装箱(每个结构体类型实际上定义了两个种类的东西:一个存储位置类型,它充当值类型;一个堆对象类型,它充当类类型;第一个可以隐式转换为第二个("装箱");第二个可以转换为首先通过显式转换-"拆箱")。但是,可以使用所谓的约束泛型来利用结构的接口实现而不用装箱。

    例如,如果一个人有方法CompareTwoThings< T >(T thing1, T thing2) where T:IComparable< T >,那么这种方法可以调用thing1.Compare(thing2),而不必将thing1thing2装箱。如果thing1恰好是Int32,则运行时将在生成CompareTwoThings(Int32 thing1, Int32 thing2)的代码时知道。由于它将知道托管该方法的事物和作为参数传递的事物的确切类型,因此不必对它们中的任何一个进行装箱。

    实现接口的结构的最大问题是,存储在接口类型ObjectValueType的位置(与其自身类型的位置相对)的结构将充当类对象。对于只读接口,这通常不是问题,但是对于像IEnumerator< T >这样的变异接口,它可能会产生一些奇怪的语义。

    例如,考虑以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    List<String> myList = [list containing a bunch of strings]
    var enumerator1 = myList.GetEnumerator();  // Struct of type List<String>.IEnumerator
    enumerator1.MoveNext(); // 1
    var enumerator2 = enumerator1;
    enumerator2.MoveNext(); // 2
    IEnumerator<string> enumerator3 = enumerator2;
    enumerator3.MoveNext(); // 3
    IEnumerator<string> enumerator4 = enumerator3;
    enumerator4.MoveNext(); // 4

    带标记的语句#1将对enumerator1进行撇号以读取第一个元素。该枚举器的状态将被复制到enumerator2。标记为#2的语句将使该副本前进以读取第二个元素,但不会影响enumerator1。然后将第二个枚举器的状态复制到enumerator3,该状态将通过标记的语句#3进行高级处理。然后,由于enumerator3enumerator4都是引用类型,因此对enumerator3的引用将被复制到enumerator4,因此带标记的语句将有效地同时推进enumerator3enumerator4

    有人试图假装值类型和引用类型都是Object类型,但这不是真的。实数值类型可以转换为Object,但不是其实例。 List.Enumerator的实例(存储在该类型的位置中)是一个值类型,其行为与值类型相同。将其复制到IEnumerator类型的位置会将其转换为引用类型,并且将充当引用类型。后者是Object的一种,但前者不是。

    顺便说一句,还有更多注意事项:(1)通常,可变类类型应使用其Equals方法测试引用的相等性,但盒装结构体没有这样做的体面方法; (2)尽管ValueType是它的名字,但它是一个类类型,而不是值类型;从System.Enum派生的所有类型都是值类型,从ValueType派生的所有类型(System.Enum除外)也都是值类型,但是ValueTypeSystem.Enum都是类类型。


    (没什么要补充的,但是还没有编辑能力,所以这里是。。)
    万无一失。在结构上实现接口没有违法行为。但是,您应该质疑为什么要这样做。

    但是,获取对结构的接口引用会将其装箱。因此,性能损失等等。

    我现在可以想到的唯一有效方案已在此处发布。当您想要修改存储在集合中的结构状态时,必须通过结构上公开的其他接口来完成。


    结构被实现为值类型,而类是引用类型。如果您具有类型为Foo的变量,并且在其中存储了Fubar的实例,它将把它"装箱"为引用类型,这样就失去了首先使用结构的优势。

    我看到使用结构而不是类的唯一原因是因为它将是值类型而不是引用类型,但是结构不能从类继承。如果您的结构继承了一个接口,并且传递了接口,那么您将失去该结构的值类型性质。如果需要接口,也可以将其设为类。


    我认为问题在于它会导致装箱,因为结构是值类型,因此会有轻微的性能损失。

    此链接表明可能还有其他问题...

    http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx


    几乎没有理由使用值类型实现接口。由于您不能继承值类型,因此您始终可以将其称为具体类型。

    当然,除非您有多个结构都实现相同的接口,否则它可能会稍微有用,但是在那一点上,我建议您使用一个类并正确地进行操作。

    当然,通过实现一个接口,您可以将结构装箱,因此它现在位于堆上,您将无法再通过值传递它...这确实加强了我的意见,即您应该只使用一个类在这种情况下。


    实现接口的结构没有任何后果。例如,内置系统结构实现诸如IComparableIFormattable之类的接口。


    结构就像存在于堆栈中的类一样。我认为没有理由为什么它们应该是"不安全的"。


    展开全文阅读

    相关内容