题
我似乎记得读过一些关于在 CLR 中通过 C# 实现结构如何不好的内容,但我似乎找不到任何相关内容。是不是很糟糕?这样做会产生意想不到的后果吗?
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
一般来说,结构应该用于具有值类型语义的对象。通过在结构上实现接口,您可能会遇到装箱问题,因为结构在结构和接口之间来回转换。由于装箱,更改结构内部状态的操作可能无法正常运行。
其他提示
由于没有其他人明确提供这个答案,我将添加以下内容:
实施 结构体上的接口不会产生任何负面影响。
任何 多变的 用于保存结构的接口类型的类型将导致使用该结构的装箱值。如果结构是不可变的(一件好事),那么这在最坏的情况下是一个性能问题,除非您:
- 使用生成的对象进行锁定(无论如何都是一个非常糟糕的主意)
- 使用引用相等语义并期望它适用于同一结构中的两个装箱值。
这两种情况都不太可能发生,相反,您可能会执行以下操作之一:
泛型
也许结构实现接口的许多合理原因是它们可以在一个 通用的 上下文与 限制条件. 。当以这种方式使用时,变量如下所示:
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);
}
}
- 允许使用结构作为类型参数
- 只要没有其他约束,比如
new()
或者class
用来。
- 只要没有其他约束,比如
- 允许避免对以这种方式使用的结构进行装箱。
那么 this.a 不是接口引用,因此它不会导致将任何内容放入其中的盒子。此外,当 c# 编译器编译泛型类并需要插入对类型参数 T 的实例上定义的实例方法的调用时,它可以使用 受约束的 操作码:
如果 thisType 是值类型并且 thisType 实现方法,则 ptr 未经修改地作为“this”指针传递到调用方法指令,以便通过 thisType 实现方法。
这避免了装箱,并且由于值类型正在实现接口 必须 实现该方法,因此不会发生装箱。在上面的例子中 Equals()
调用是在 this.a 上没有任何框的情况下完成的1.
低摩擦 API
大多数结构应该具有类似原始的语义,其中按位相同的值被认为是相等的2. 。运行时将隐式提供此类行为 Equals()
但这可能会很慢。这个隐式的等式也是 不是 暴露为一个实现 IEquatable<T>
从而防止结构被轻易地用作字典的键,除非它们自己明确地实现它。因此,许多公共结构类型通常声明它们实现 IEquatable<T>
(在哪里 T
是他们自己),使这更容易、更好地执行,并且与 CLR BCL 中许多现有值类型的行为保持一致。
BCL 中的所有原语至少实现:
IComparable
IConvertible
IComparable<T>
IEquatable<T>
(因此IEquatable
)
许多人还实施 IFormattable
, ,此外,许多系统定义的值类型(例如 DateTime、TimeSpan 和 Guid)也实现了其中的许多或全部。如果您正在实现类似的“广泛使用”类型,例如复数结构或某些固定宽度文本值,那么(正确地)实现许多这些通用接口将使您的结构更加有用和可用。
排除情况
显然,如果界面强烈暗示 可变性 (例如 ICollection
)然后实现它是一个坏主意,因为这意味着您要么使结构可变(导致已经描述的各种错误,其中修改发生在装箱值而不是原始值上),或者您通过忽略以下含义来迷惑用户像这样的方法 Add()
或抛出异常。
许多接口并不意味着可变性(例如 IFormattable
)并作为以一致的方式公开某些功能的惯用方式。通常,结构的用户不会关心此类行为的任何装箱开销。
概括
如果明智地完成,对于不可变值类型,实现有用的接口是一个好主意
笔记:
1:请注意,当对变量调用虚拟方法时,编译器可能会使用它 已知的 属于特定的结构类型,但需要调用虚拟方法。例如:
List<int> l = new List<int>();
foreach(var x in l)
;//no-op
List 返回的枚举器是一个结构体,这是一种优化,以避免在枚举列表时分配(有一些有趣的 结果)。然而 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: 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 值不被视为相等。
在某些情况下,结构体实现接口可能是件好事(如果它从来没有用过,那么 .net 的创建者是否会提供它就值得怀疑了)。如果一个结构体实现了一个只读接口,例如 IEquatable<T>
, ,将结构体存储在类型的存储位置(变量、参数、数组元素等)中 IEquatable<T>
需要将其装箱(每个结构类型实际上定义了两种东西:充当值类型的存储位置类型和充当类类型的堆对象类型;第一个可以隐式转换为第二个——“装箱”——而第二个可以通过显式转换转换为第一个——“拆箱”)。然而,使用所谓的约束泛型,可以在不装箱的情况下利用接口的结构实现。
例如,如果有人有一种方法 CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>
, ,这样的方法可以调用 thing1.Compare(thing2)
无需装箱 thing1
或者 thing2
. 。如果 thing1
恰好是,例如 Int32
, ,运行时会知道当它生成代码时 CompareTwoThings<Int32>(Int32 thing1, Int32 thing2)
. 。由于它知道托管该方法的事物和作为参数传递的事物的确切类型,因此不必对它们中的任何一个进行装箱。
实现接口的结构体的最大问题是存储在接口类型位置的结构体, Object
, , 或者 ValueType
(而不是其自身类型的位置)将表现为类对象。对于只读接口,这通常不是问题,但对于像这样的变异接口 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 推进。那么,因为 enumerator3
和 enumerator4
都是引用类型,a 参考 到 enumerator3
然后将被复制到 enumerator4
, ,因此标记的语句将有效推进 两个都 enumerator3
和 enumerator4
.
有些人试图假装值类型和引用类型都是 Object
, ,但事实并非如此。实值类型可以转换为 Object
, ,但不是它的实例。一个实例 List<String>.Enumerator
存储在该类型的位置的是值类型并且表现为值类型;将其复制到类型的位置 IEnumerator<String>
将其转换为引用类型,并且 它将表现为引用类型. 。后者是一种 Object
, ,但前者不是。
顺便说一句,还有一些注意事项:(1) 一般来说,可变类类型应该有自己的 Equals
方法测试引用相等性,但装箱结构没有合适的方法来做到这一点;(2) 尽管有它的名字, ValueType
是类类型,而不是值类型;所有类型都派生自 System.Enum
是值类型,所有派生自的类型也是 ValueType
除了 System.Enum
, ,但两者都 ValueType
和 System.Enum
是类类型。
结构体作为值类型实现,而类是引用类型。如果您有一个 Foo 类型的变量,并且在其中存储了 Fubar 的实例,它会将其“装箱”为引用类型,从而首先消除了使用结构体的优势。
我认为使用结构而不是类的唯一原因是因为它将是值类型而不是引用类型,但结构不能从类继承。如果您让结构继承接口,并且传递接口,那么您将失去结构的值类型性质。如果您需要接口,不妨将其作为一个类。
(好吧,没有什么重要的补充,但还没有编辑能力,所以这里......)
万无一失。在结构上实现接口没有任何违法行为。但是,您应该质疑为什么要这样做。
然而 获取对结构体的接口引用 will BOX 它。所以性能惩罚等等。
我现在能想到的唯一有效的场景是 我的帖子中有说明. 。当您想要修改存储在集合中的结构的状态时,您必须通过结构上公开的附加接口来完成。
我认为问题在于它会导致装箱,因为结构是值类型,因此会有轻微的性能损失。
此链接表明可能还有其他问题......
http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
实现接口的结构不会产生任何后果。例如,内置系统结构实现如下接口 IComparable
和 IFormattable
.
值类型实现接口的理由很少。由于您无法对值类型进行子类化,因此您始终可以将其作为其具体类型进行引用。
当然,除非您有多个结构都实现相同的接口,否则它可能会有一点用处,但那时我建议使用一个类并正确执行。
当然,通过实现接口,您正在对结构进行装箱,因此它现在位于堆上,并且您将无法再按值传递它......这确实强化了我的观点,即您应该只使用一个类在这个情况下。
结构就像存在于堆栈中的类一样。我认为他们没有理由“不安全”。