it-swarm.cn

结构体实现接口是否安全?

我似乎记得读过一些关于结构通过C#在CLR中实现接口有什么不好的东西,但我似乎无法找到任何关于它的东西。这不好吗?这样做会产生意想不到的后果吗?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
81
Will

这个问题有几个问题......

结构体可以实现接口,但是存在关于转换,可变性和性能的问题。有关详细信息,请参阅此帖子: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

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

42
Scott Dorman

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

实现 结构上的接口没有任何负面后果。

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

  • 使用生成的对象进行锁定(无论如何都是一个非常糟糕的主意)
  • 使用引用相等语义并期望它适用于来自同一结构的两个盒装值。

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

泛型

结构化实现接口的许多合理原因可能是它们可以在带有constraints generic context中使用。当以这种方式使用时,变量如下:

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);
    }
}
  1. 允许使用struct作为类型参数
    • 只要没有使用像new()classname__这样的其他约束。
  2. 允许在这种方式使用的结构上避免装箱。

然后this.a不是一个接口引用,因此它不会导致放入其中的任何内容。此外,当c#编译器编译泛型类并需要插入在Type参数T的实例上定义的实例方法的调用时,它可以使用 constrained opcode:

如果thisType是一个值类型而thisType实现了方法,那么ptr将被未修改地传递为调用方法指令的'this'指针,用于通过thisType实现方法。

这避免了装箱,因为值类型实现接口是必须实现方法,因此不会发生装箱。在上面的例子中,Equals()调用在this.a上没有任何框1

低摩擦API

大多数结构应该具有类似原始的语义,其中按位相同的值被认为是相等的2。运行时将在隐式Equals()中提供此类行为,但这可能很慢。此隐式相等也是not作为IEquatable<T>的实现公开,因此可以防止结构容易用作字典的键,除非它们自己明确地实现它。因此,许多公共结构类型通常声明它们实现IEquatable<T>(其中Tname__是它们自己)以使其更容易和更好地执行以及与CLR BCL内的许多现有值类型的行为一致。

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

  • IComparablename__
  • IConvertiblename__
  • IComparable<T>
  • IEquatable<T>(因而IEquatablename__)

许多还实现IFormattablename__,更多的系统定义的值类型,如DateTime,TimeSpan和Guid也实现了许多或所有这些。如果您正在实现类似“广泛有用”的类型,如复数结构或某些固定宽度的文本值,那么实现许多这些通用接口(正确)将使您的结构更有用和可用。

排除

显然,如果接口强烈暗示mutability(例如ICollectionname__),那么实现它是一个坏主意,因为它意味着你要么使结构可变(导致已经描述的修改发生的错误的种类)盒装值而不是原始值)或者您通过忽略Add()或抛出异常等方法的含义来混淆用户。

许多接口并不意味着可变性(例如IFormattablename__),并且作为以一致方式公开某些功能的惯用方式。结构的用户通常不会关心此类行为的任何装箱开销。

摘要

当理性地完成时,对于不可变值类型,实现有用的接口是个好主意


笔记:

1:请注意,在对known的变量调用虚拟方法时,编译器可能会使用此方法,这些变量属于特定的结构类型,但需要调用虚方法。例如:

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

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

 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.sIL_0019 
 IL_0011:ldloca.s 02 
 IL_0013:调用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:受限制。 System.Collections.Generic.List.Enumerator 
 IL_002E:callvirt System.IDisposable.Dispose 
 IL_0033:nop 
 IL_0034:endfinally 

因此,IDisposable的实现不会导致任何性能问题,并且如果Dispose方法实际上做任何事情,则保留枚举器的(可遗憾的)可变方面!

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

158
ShuggyCoUk

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

例如,如果有一个方法CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>,这样的方法可以调用thing1.Compare(thing2)而不必使用thing1thing2。如果thing1碰巧是例如Int32,则运行时将知道它何时生成CompareTwoThings<Int32>(Int32 thing1, Int32 thing2)的代码。因为它将知道托管方法的东西和作为参数传递的东西的确切类型,所以它不必包装它们中的任何一个。

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

例如,考虑以下代码:

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都是引用类型,所以REFERENCEenumerator3将被复制到enumerator4,因此标记语句将有效地推进bothenumerator3enumerator4

有些人试图假装值类型和引用类型都是Object,但事实并非如此。实值类型可转换为Object,但不是它的实例。存储在该类型位置的List<String>.Enumerator实例是值类型,其行为类型为值;将其复制到IEnumerator<String>类型的位置会将其转换为引用类型,并且它将表现为引用类型。后者是一种Object,但前者不是。

顺便说一下,还有一些注意事项:(1)一般来说,可变类类应该有Equals方法测试引用相等,但是盒装结构没有合适的方法可以做到这一点。 (2)尽管名称如此,ValueType是一个类类型,而不是一个值类型;从System.Enum派生的所有类型都是值类型,从ValueType派生的所有类型都是System.Enum,但ValueTypeSystem.Enum都是类类型。

8
supercat

(没有什么可以添加的主要内容但是还没有编辑能力,所以这里......)
完全安全。在结构上实现接口没有任何违法行为。但是你应该质疑为什么要这样做。

但是 获取结构的接口引用将BOX it。所以性能损失等等。

我现在能想到的唯一有效场景是 在我的帖子中说明 。如果要修改存储在集合中的结构状态,则必须通过结构上公开的其他接口来执行此操作。

3
Gishu

结构实现为值类型,类是引用类型。如果你有一个Foo类型的变量,并且你在其中存储了一个Fubar实例,它会将它“装箱”成一个引用类型,从而破坏了首先使用结构的优势。

我看到使用结构而不是类的唯一原因是因为它将是一个值类型而不是引用类型,但结构不能从类继承。如果您有结构继承接口,并且传递接口,则会丢失结构的值类型性质。如果你需要接口,也可以让它成为一个类。

3
dotnetengineer

我认为问题是它导致装箱因为结构是值类型所以有轻微的性能损失。

此链接表明可能存在其他问题......

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

1
Simon Keep

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

0
Joseph Daigle

值类型实现接口的理由很少。由于您不能对值类型进行子类化,因此始终可以将其称为其具体类型。

当然,除非你有多个结构都实现了相同的接口,否则它可能稍微有用,但在那时我建议使用一个类并正确地执行它。

当然,通过实现一个接口,你正在装结结构,所以它现在位于堆上,你将无法再按值传递它......这真的强化了我的观点,你应该只使用一个类在这种情况下。

0
FlySwat