结构体实现接口是否安全?
我似乎还记得阅读关于结构如何通过C#实现CLR中的接口是不好的,但我似乎无法find任何有关它的信息。 这不好吗? 这样做是否有意想不到的后果?
public interface Foo { Bar GetBar(); } public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
在这个问题上有几件事情…
一个结构可以实现一个接口,但是有关于铸造,可变性和性能的问题。 有关更多详细信息,请参阅此文章: http : //blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
通常,结构体应该用于具有值types语义的对象。 通过在一个结构上实现一个接口,你可以在结构和接口之间来回转换结构时遇到装箱问题。 作为拳击的结果,改变结构的内部状态的操作可能不正常。
由于没有人明确提供这个答案,我将添加以下内容:
在结构上实现一个接口不会有任何负面的后果。
用于存放结构的接口types的任何variables都将导致使用该结构的装箱值。 如果结构是不可变的(好东西),那么这是最糟糕的性能问题,除非你是:
- 使用结果对象进行locking(任何方式都是一个非常糟糕的主意)
- 使用引用相等语义,并期望它从同一个结构的两个盒装值。
这两个都不太可能,相反,你可能会做以下其中一个:
generics
也许结构实现接口的许多合理的原因是它们可以在具有约束的通用上下文中使用。 当以这种方式使用variables像这样:
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); } }
- 启用使用该结构作为types参数
- 只要不使用
new()
或class
等其他约束条件即可。
- 只要不使用
- 允许避免在这种方式使用的结构拳击。
然后this.a不是一个接口引用,因此它不会导致任何被放入它的盒子。 进一步,当c#编译器编译generics类并需要插入在Type参数T的实例上定义的实例方法的调用时,它可以使用受约束的操作码:
如果thisType是一个值types,并且thisType实现了方法,那么ptr作为“this”指针被传递给一个调用方法指令,以便由thisType实现方法。
这避免了拳击,并且由于值types正在实现,所以接口必须实现该方法,因此不会发生装箱。 在上面的例子中, Equals()
调用在this.a 1上没有框。
低摩擦的API
大多数结构应该具有原始类似的语义,其中按位相同的值被认为是相等的2 。 运行时将在隐式的Equals()
提供这种行为,但这可能会很慢。 另外,这种隐式的平等不作为IEquatable<T>
的实现IEquatable<T>
,因此可以防止结构被轻易地用作字典的键,除非它们自己明确地实现它。 因此,许多公共结构types通常会声明它们实现了IEquatable<T>
(其中T
是它自己),以使其更容易和更好地执行,并且与CLR BCL中许多现有值types的行为一致。
BCL中的所有基元都至less实施:
-
IComparable
-
IConvertible
-
IComparable<T>
-
IEquatable<T>
(因此IEquatable
)
许多也实现了IFormattable
,更多的系统定义的值types如DateTime,TimeSpan和Guid也实现了许多或所有这些。 如果您正在实现类似“广泛使用”的types(如复数结构或一些固定宽度的文本值),那么实现这些常用接口(正确)将使您的结构更加有用和可用。
排除
显然,如果接口强烈地暗示了可变性 (比如ICollection
),那么实现它就是一个坏主意,因为这意味着你要么使得结构可变(导致已经在盒装值上发生修改而已经描述的各种错误)原始的),或者通过忽略Add()
或抛出exception等方法的影响来混淆用户。
许多接口并不意味着可变性(如IFormattable
),而是作为以一致的方式公开某些function的惯用方式。 通常结构的用户不会在乎这种行为的任何装箱开销。
概要
当明智地完成,在不可变的值types上,有用的接口的实现是一个好主意
笔记:
1:请注意,编译器可能会在调用已知为特定结构types但需要调用虚拟方法的variables的虚拟方法时使用此方法。 例如:
List<int> l = new List<int>(); foreach(var x in l) ;//no-op
列表返回的枚举器是一个结构体,在列举列表时避免分配的优化(带有一些有趣的结果 )。 然而,foreach的语义指定如果枚举器实现了IDisposable
那么一旦迭代完成, 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.s IL_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:调用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:最终
因此,IDisposable的实现不会导致任何性能问题,如果Dispose方法实际上做了任何事情,那么枚举数的(可惜的)可变方面将被保留!
2:double和float是这个规则的例外,其中NaN值不被认为是相等的。
在某些情况下,对于一个结构来说,实现一个接口可能是件好事(如果它没有用处,那么.net的创build者就可以提供这个接口了)。 如果一个结构像IEquatable<T>
那样实现一个只读接口,那么将该结构存储在IEquatable<T>
的存储位置(variables,参数,数组元素等)中将需要将其IEquatable<T>
(实际上每个结构types定义了两种东西:作为一个值types的存储位置types和作为类types的堆对象types;第一个隐式转换为第二个 – “装箱” – 第二个可以转换到第一个通过明确的演员 – “拆箱”)。 然而,使用所谓的约束generics,可以利用一个没有装箱的接口的结构实现。
例如,如果有一个方法CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>
,则此方法可以调用thing1.Compare(thing2)
而不必将thing1
或thing2
框中。 如果thing1
碰巧是一个Int32
,那么运行时就会知道当它为CompareTwoThings<Int32>(Int32 thing1, Int32 thing2)
生成代码时。 因为它将知道托pipe方法的东西和作为parameter passing的东西的确切types,所以它不需要将它们中的任何一个框起来。
实现接口的结构最大的问题是,存储在接口types, Object
或者值types(而不是它自己types的位置)的位置的结构将performance为一个类对象。 对于只读接口,这通常不是一个问题,但对于像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
1读取第一个元素。 该枚举器的状态将被复制到enumerator2
。 标记的语句#2将提前该副本读取第二个元素,但不会影响enumerator1
。 第二个枚举器的状态将被复制到enumerator3
,这将通过标记语句#3来提前。 然后,由于enumerator3
和enumerator4
器4都是引用types,因此对enumerator3
将被复制到enumerator4
,因此标记语句将有效地推进enumerator3
和enumerator4
。
有些人试图假装值types和引用types都是Object
种类,但这不是真的。 真正的值types可以转换为Object
,但不是它的实例。 存储在该types位置的List<String>.Enumerator
一个实例是一个值types,并且performance为一个值types; 将其复制到IEnumerator<String>
types的位置将会将其转换为引用types,并且将作为引用types 。 后者是一种Object
,但前者不是。
顺便说一句,一些更多的注意事项:(1)一般来说,可变类的types应该有他们的等式方法testing引用相等,但没有体面的方式为盒装结构这样做; (2)尽pipe它的名字, ValueType
是一个类的types,而不是一个值types; 从System.Enum
派生的所有types都是值types,所有从ValueType
派生的types( System.Enum
除外),但ValueType
和System.Enum
都是类types。
结构被实现为值types,类是引用types。 如果你有一个types为Foo的variables,并且在其中存储了一个Fubar的实例,它会将其“封装”到一个引用types中,从而破坏了首先使用结构的好处。
我看到使用结构而不是类的唯一原因是因为它将是一个值types,而不是一个引用types,但结构不能从类inheritance。 如果你的结构inheritance了一个接口,并且你传递了接口,那么你将失去该结构的值types性质。 如果你需要接口,不妨把它做成一个类。
(没有什么专业添加,但没有编辑的实力,所以这里去..)
万无一失。 在结构上实现接口没有任何违法的地方。 但是,你应该问为什么你想这样做。
然而获得一个结构的接口引用将会把它插入 。 所以performance的惩罚等等。
我现在唯一能想到的唯一有效scheme就是在我的文章中 。 当你想修改一个存储在一个集合中的结构的状态时,你必须通过结构上暴露的额外接口来完成。
我认为问题是它会导致拳击,因为结构是价值types,所以有一个轻微的性能损失。
这个链接表明可能还有其他问题
http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
对实现接口的结构没有任何影响。 例如,内置的系统结构实现了IComparable
和IFormattable
等接口。
值types实现接口的原因很less。 既然你不能inheritance一个值types,你总是可以引用它作为它的具体types。
当然,除非你有多个实现相同接口的结构体,否则它可能稍微有用,但是在那一点上,我build议使用一个类并且正确地做。
当然,通过实现一个接口,你正在装箱的结构,所以它现在坐在堆上,你将无法再通过它的价值了…这真的强化了我的意见,你应该只使用一个类在这个情况下。
结构就像堆栈中的类一样。 我看不出为什么他们应该是“不安全的”。