我似乎还记得读过一些有关结构如何通过C#在CLR中实现接口的弊端,但是我似乎找不到任何东西。不好吗 这样做会有意想不到的后果吗?
public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
Answers:
这个问题有几件事发生...
结构可以实现接口,但转换,可变性和性能会引起关注。请参阅此帖子以获取更多详细信息:https : //docs.microsoft.com/zh-cn/archive/blogs/abhinaba/c-structs-and-interface
通常,结构应用于具有值类型语义的对象。通过在结构上实现接口,您会在结构与接口之间来回转换时遇到拳击问题。装箱的结果是,更改结构内部状态的操作可能无法正常运行。
IComparable<T>
和IEquatable<T>
。将结构存储Foo
在类型变量IComparable<Foo>
中将需要装箱,但是如果将通用类型T
限制为IComparable<T>
一个,则可以将其与另一个进行比较,T
而不必将任何一个都装箱,也不必T
了解除实现约束之外的任何其他信息。只有通过结构实现接口的能力才能使这种有利行为成为可能。话虽如此……
由于没有其他人明确提供此答案,因此我将添加以下内容:
在结构上实现接口不会产生任何负面影响。
用于保存结构的接口类型的任何变量都将导致使用该结构的框式值。如果该结构是不可变的(一件好事),那么除非您是:
这两种情况均不太可能,相反,您可能正在执行以下一项操作:
结构实现接口的许多合理原因也许是,以便可以在具有约束的通用上下文中使用它们。以这种方式使用变量时,如下所示:
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#编译器编译通用类并需要插入在Type参数T的实例上定义的实例方法的调用时,它可以使用受约束的操作码:
如果thisType是值类型,并且thisType实现方法,则将ptr未经修改地传递为调用方法指令的'this'指针,以供thisType实现方法。
这避免了装箱,并且由于值类型正在实现,接口必须实现该方法,因此不会发生装箱。在上面的示例Equals()
中,在this.a 1上没有任何框的情况下完成了调用。
大多数结构应具有类似原始的语义,其中按位相同的值被视为等于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:请注意,当对已知具有特定结构类型但需要调用虚拟方法的变量调用虚拟方法时,编译器可能会使用此方法。例如:
List<int> l = new List<int>();
foreach(var x in l)
;//no-op
List返回的枚举数是一个结构,是一种优化方法,可以避免在枚举列表时进行分配(具有一些有趣的结果)。但是,foreach的语义指定如果枚举器实现,IDisposable
则Dispose()
在迭代完成后将调用该枚举器。显然,通过框式调用来实现此功能将消除枚举数为struct的任何好处(实际上会更糟)。更糟糕的是,如果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:兄弟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:离开。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值不相等。
struct
到interface
。
在某些情况下,结构实现接口可能会很好(如果它从没有用过,那么.net的创建者是否会为此提供接口值得怀疑)。如果一个结构体实现一个只读接口,如IEquatable<T>
,将其存储在类型的存储位置(变量,参数,数组元素等)中,IEquatable<T>
将要求将其装箱(每个结构体类型实际上定义了两种东西:存储)用作值类型的位置类型和用作类类型的堆对象类型;第一个可隐式转换为第二个(“装箱”),第二个可通过显式强制转换为第一个。 “取消装箱”)。但是,可以使用所谓的约束泛型来利用结构的接口实现而不用装箱。
例如,如果一个人有一个方法CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>
,那么这种方法可以调用thing1.Compare(thing2)
而不必使用boxthing1
或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
都是引用类型,一个参考,以enumerator3
将被复制到enumerator4
,如此显着的语句将有效地促进双方 enumerator3
和enumerator4
。
有人试图假装值类型和引用类型都是的Object
,但这不是真的。实值类型可以转换为Object
,但不是实例。List<String>.Enumerator
存储在该类型位置的实例是值类型,其行为与值类型相同。将其复制到type的位置IEnumerator<String>
会将其转换为引用类型,并且将充当引用类型。后者是一种Object
,但前者不是。
顺便说一句,还有一些注意事项:(1)一般而言,可变类类型应让其Equals
方法测试引用相等性,但是盒装结构没有这样做的体面方法;(2)尽管名称,但它ValueType
是类类型,而不是值类型;衍生自所有类型System.Enum
是值类型,因为是从派生的所有类型ValueType
的除外System.Enum
,但两者ValueType
和System.Enum
是类的类型。
我认为问题在于它会导致装箱,因为结构是值类型,因此会有轻微的性能损失。
此链接表明可能还有其他问题...
http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
值类型实现接口的理由很少。由于您不能继承值类型,因此您始终可以将其称为具体类型。
当然,除非您有多个结构都实现相同的接口,否则它可能会有点用,但是在那一点上,我建议您使用一个类并正确地进行操作。
当然,通过实现一个接口,您可以将结构装箱,因此它现在位于堆上,您将无法再通过值传递它...这确实加强了我的意见,即您应该只使用一个类在这个情况下。
IComparable
四处走动来装箱值。通过简单地调用IComparable
期望值的方法来实现它,您将隐式装箱该值类型。
IComparable<T>
在类型的结构上调用方法T
。
结构就像存在于堆栈中的类一样。我认为没有理由为什么它们应该是“不安全的”。