我似乎记得读过一些关于结构通过C#在CLR中实现接口有什么不好的东西,但我似乎无法找到任何关于它的东西.这不好吗?这样做会产生意想不到的后果吗?
public interface Foo { Bar GetBar(); } public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
ShuggyCoUk.. 165
由于没有其他人明确提供此答案,我将添加以下内容:
在结构上实现接口不会产生任何负面影响.
用于保存结构的接口类型的任何变量都将导致使用该结构的盒装值.如果结构是不可变的(一件好事),那么这是最糟糕的性能问题,除非你是:
使用生成的对象进行锁定(无论如何都是一个非常糟糕的主意)
使用引用相等语义并期望它适用于来自同一结构的两个盒装值.
这两种情况都不太可能,相反,您可能正在执行以下操作之一:
结构化实现接口的许多合理原因可能是它们可以在具有约束的通用上下文中使用.当以这种方式使用时,变量如下:
class Foo: IEquatable > where T : IEquatable { private readonly T a; public bool Equals(Foo other) { return this.a.Equals(other.a); } }
允许将struct用作类型参数
只要不使用new()
或class
使用其他约束.
允许在这种方式使用的结构上避免装箱.
然后this.a不是一个接口引用,因此它不会导致放入其中的任何内容.此外,当c#编译器编译泛型类并需要插入在Type参数T的实例上定义的实例方法的调用时,它可以使用受约束的操作码:
如果thisType是一个值类型而thisType实现了方法,那么ptr将被未修改地传递为调用方法指令的'this'指针,用于通过thisType实现方法.
这避免了装箱,并且由于值类型是实现接口必须实现该方法,因此不会发生装箱.在上面的例子中,Equals()
调用是在没有框的情况下完成的.a 1.
大多数结构应该具有类似原始的语义,其中按位相同的值被认为是相等的2.运行时将以隐式方式提供此类行为,Equals()
但这可能很慢.此隐式相等也不作为实现公开IEquatable
,因此可以防止结构容易用作字典的键,除非它们自己明确地实现它.因此,许多公共结构类型通常声明它们实现IEquatable
(T
它们在哪里)以使其更容易和更好地执行,以及与CLR BCL中的许多现有值类型的行为一致.
BCL中的所有原语都至少实现:
IComparable
IConvertible
IComparable
IEquatable
(因而IEquatable
)
许多还实现了IFormattable
,许多系统定义的值类型,如DateTime,TimeSpan和Guid也实现了许多或所有这些.如果您正在实现类似"广泛有用"的类型,如复数结构或某些固定宽度的文本值,那么实现许多这些通用接口(正确)将使您的结构更有用和可用.
显然,如果接口强烈暗示可变性(例如ICollection
),那么实现它是一个坏主意,因为它意味着您要么使结构可变(导致已经描述的类型的错误,其中修改发生在盒装值而不是原始或者您通过忽略方法的含义Add()
或抛出异常来混淆用户.
许多接口并不意味着可变性(例如IFormattable
),并且作为以一致方式公开某些功能的惯用方式.结构的用户通常不会关心此类行为的任何装箱开销.
当理性地完成时,对于不可变值类型,实现有用的接口是个好主意
1:请注意,编译器可以在对已知具有特定结构类型但需要调用虚方法的变量调用虚方法时使用此方法.例如:
Listl = new List (); 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值不被视为相等.
由于没有其他人明确提供此答案,我将添加以下内容:
在结构上实现接口不会产生任何负面影响.
用于保存结构的接口类型的任何变量都将导致使用该结构的盒装值.如果结构是不可变的(一件好事),那么这是最糟糕的性能问题,除非你是:
使用生成的对象进行锁定(无论如何都是一个非常糟糕的主意)
使用引用相等语义并期望它适用于来自同一结构的两个盒装值.
这两种情况都不太可能,相反,您可能正在执行以下操作之一:
结构化实现接口的许多合理原因可能是它们可以在具有约束的通用上下文中使用.当以这种方式使用时,变量如下:
class Foo: IEquatable > where T : IEquatable { private readonly T a; public bool Equals(Foo other) { return this.a.Equals(other.a); } }
允许将struct用作类型参数
只要不使用new()
或class
使用其他约束.
允许在这种方式使用的结构上避免装箱.
然后this.a不是一个接口引用,因此它不会导致放入其中的任何内容.此外,当c#编译器编译泛型类并需要插入在Type参数T的实例上定义的实例方法的调用时,它可以使用受约束的操作码:
如果thisType是一个值类型而thisType实现了方法,那么ptr将被未修改地传递为调用方法指令的'this'指针,用于通过thisType实现方法.
这避免了装箱,并且由于值类型是实现接口必须实现该方法,因此不会发生装箱.在上面的例子中,Equals()
调用是在没有框的情况下完成的.a 1.
大多数结构应该具有类似原始的语义,其中按位相同的值被认为是相等的2.运行时将以隐式方式提供此类行为,Equals()
但这可能很慢.此隐式相等也不作为实现公开IEquatable
,因此可以防止结构容易用作字典的键,除非它们自己明确地实现它.因此,许多公共结构类型通常声明它们实现IEquatable
(T
它们在哪里)以使其更容易和更好地执行,以及与CLR BCL中的许多现有值类型的行为一致.
BCL中的所有原语都至少实现:
IComparable
IConvertible
IComparable
IEquatable
(因而IEquatable
)
许多还实现了IFormattable
,许多系统定义的值类型,如DateTime,TimeSpan和Guid也实现了许多或所有这些.如果您正在实现类似"广泛有用"的类型,如复数结构或某些固定宽度的文本值,那么实现许多这些通用接口(正确)将使您的结构更有用和可用.
显然,如果接口强烈暗示可变性(例如ICollection
),那么实现它是一个坏主意,因为它意味着您要么使结构可变(导致已经描述的类型的错误,其中修改发生在盒装值而不是原始或者您通过忽略方法的含义Add()
或抛出异常来混淆用户.
许多接口并不意味着可变性(例如IFormattable
),并且作为以一致方式公开某些功能的惯用方式.结构的用户通常不会关心此类行为的任何装箱开销.
当理性地完成时,对于不可变值类型,实现有用的接口是个好主意
1:请注意,编译器可以在对已知具有特定结构类型但需要调用虚方法的变量调用虚方法时使用此方法.例如:
Listl = new List (); 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值不被视为相等.
这个问题有几个问题......
结构体可以实现接口,但是存在关于转换,可变性和性能的问题.有关更多详细信息,请参阅此帖子:http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
通常,结构应该用于具有值类型语义的对象.通过在结构上实现接口,您可以遇到装箱问题,因为结构在结构和接口之间来回转换.作为装箱的结果,更改结构的内部状态的操作可能无法正常运行.
在某些情况下,结构实现一个接口可能是好事(如果它从来没有用过,那么.net的创建者会提供它是否值得怀疑).如果一个struct实现了一个只读接口IEquatable
,那么将struct存储在类型的存储位置(变量,参数,数组元素等)中IEquatable
就需要将它装箱(每个struct类型实际上定义了两种类型:存储作为值类型的行为类型和作为类类型行为的堆对象类型;第一个可隐式转换为第二个 - "拳击" - 第二个可以通过显式转换转换为第一个 - "拆箱").但是,可以使用所谓的约束泛型来利用结构的接口实现而无需装箱.
例如,如果有一个方法CompareTwoThings
,这样的方法可以调用thing1.Compare(thing2)
而不必使用方法thing1
或thing2
.如果thing1
碰巧是,例如,Int32
运行时将知道它何时生成代码CompareTwoThings
.因为它将知道托管方法的东西和作为参数传递的东西的确切类型,所以它不必包装它们中的任何一个.
实现接口的结构的最大问题是,存储在接口类型的位置中的结构Object
,或ValueType
(与其自身类型的位置相对)将表现为类对象.对于只读接口,这通常不是问题,但对于像IEnumerator
它这样的变异接口可能会产生一些奇怪的语义.
例如,考虑以下代码:
ListmyList = [list containing a bunch of strings] var enumerator1 = myList.GetEnumerator(); // Struct of type List .IEnumerator enumerator1.MoveNext(); // 1 var enumerator2 = enumerator1; enumerator2.MoveNext(); // 2 IEnumerator enumerator3 = enumerator2; enumerator3.MoveNext(); // 3 IEnumerator enumerator4 = enumerator3; enumerator4.MoveNext(); // 4
标记语句#1将enumerator1
准备读取第一个元素.该枚举器的状态将被复制到enumerator2
.标记语句#2将使该副本前进以读取第二个元素,但不会影响enumerator1
.然后将复制第二个枚举器的状态,该状态enumerator3
将由标记的语句#3提前.然后,因为enumerator3
和enumerator4
都是引用类型,一个参考,以enumerator3
将被复制到enumerator4
,如此显着的语句将有效地促进双方 enumerator3
和enumerator4
.
有些人试图假装值类型和引用类型都是这两种类型Object
,但事实并非如此.实值类型可以转换为Object
,但不是它的实例.其实例List
存储在该类型的位置是值类型并且表现为值类型; 将其复制到类型的位置IEnumerator
会将其转换为引用类型,它将作为引用类型.后者是一种Object
,但前者不是.
顺便说一下,还有一些注意事项:(1)一般来说,可变类类应该让它们的Equals
方法测试引用相等,但是盒装结构没有合适的方法可以做到这一点.(2)尽管它的名字,ValueType
是一个类类型,而不是一个值类型; 衍生自所有类型System.Enum
是值类型,因为是从派生的所有类型ValueType
的除外System.Enum
,但两者ValueType
和System.Enum
是类的类型.