9

我想在 C# 中创建一个具有类似语义的值的类型。它是不可变的,并且内存占用少。但是,它主要是通过它实现的接口来访问的。在这种情况下,必须将值类型装箱,这意味着必须将实际值从堆栈复制到堆。因此,我想知道使用值类型(结构)而不是引用类型(类)是否有任何优势?

I为了说明我的情况,我提供了以下具有实现ReferenceTypeImplementation和的接口示例ValueTypeImplementation

interface I
{
    int GetSomeInt();
}

class ReferenceTypeImplementation : I
{
    public readonly int i;

    public ReferenceTypeImplementation (int i)
    {
        this.i = i;
    }

    public int GetSomeInt()
    {
        return i*2;
    }
}

struct ValueTypeImplementation : I
{
    public readonly int i;

    public ValueTypeImplementation (int i)
    {
        this.i = i;
    }

    public int GetSomeInt()
    {
        return i*2;
    }
}

我几乎只使用这些类型的接口I,比如

I i1 = new ReferenceTypeImplementation(1);
I i2 = new ValueTypeImplementation(1);

使用ValueTypeImplementationover有什么好处ReferenceTypeImplementation吗?

4

3 回答 3

10

与 ReferenceTypeImplementation 相比,使用 ValueTypeImplementation 有什么优势吗?

如果您要通过界面使用它,则不会。正如您所提到的,这会将值类型装箱,从而否定任何潜在的性能改进。

此外,您预期的主要用途将通过接口这一事实表明您期待引用语义,这再次表明class在这种情况下 a 会更合适。

于 2013-05-30T17:01:57.883 回答
2

将结构转换为接口会导致装箱。在结构上调用隐式实现的成员不会导致装箱:

interface I { void Foo(); }
struct S : I { public void Foo() {} }

S s = new S();
s.Foo(); // No boxing.

I i = s; // Box occurs when casting to interface.
i.Foo();

在您的情况下,如果您可以使用隐式实现/调用,那么您最好使用 struct ,因为您正在避免;

(一) 拳击

(b) 没有额外的内存开销(适用于任何引用类型分配)。

于 2013-05-30T17:07:51.147 回答
0

如果 type Fooimplements IBar,则有两种方法IBar可以在 的实例上使用的成员Foo

  • 对实例的引用(对于类类型)或对持有实例副本的堆对象的引用(对于值类型)可以存储在类型的变量中IBar

  • 实例(对于值类型)或对它的引用(对于类类型)可以存储在受限于 的泛型类型的变量(或字段、数组元素或其他存储位置)中IBar

类类型或接口类型的存储位置将保存对堆对象的引用,或null. 原始值类型的存储位置将保存表示其值的位集合。非原始值类型的存储位置将保存其所有公共和私有实例字段的连接内容。如果是类类型或接口类型,泛型类型的存储位置T将保存堆引用T,如果是原始值类型,则表示原始值的位集合,如果是结构类型,则保存 a的字段T的连接值; 此确定基于 的实际类型,而不是其任何约束。TTT

回到类型FooIBar,将 a 存储Foo在 anIBar中将导致系统创建一个新的堆对象,该对象将保存 的所有公共和私有字段的连接内容Foo,并将对该堆对象的引用存储在IBar. 然后,该堆对象的行为将与任何其他堆对象一样,而不是像值类型的存储位置。有些东西有时表现得像值类型,有时又像类类型,这可能会让人感到困惑;最好尽可能避免这种混合语义。

结构实现接口的主要时间可能是在使用遵循上述第二种模式的情况下。例如,可能有一个方法:

// Return 1, 2, or 3 depending upon how many matching or different things there are
int CountUniqueThings<T>(T first, T second, T third) where T:IEquatable<T>

在这种情况下,如果要调用CountUniqueThings(4, 6, 6),系统可以IEquatable<System.Int32>.Equals(System.Int32)直接在传入的 type 参数上调用该方法System.Int32。如果该方法已被声明:

int CountUniqueThings(IEquatable<System.Int32> first, 
                      IEquatable<System.Int32> second,
                      IEquatable<System.Int32> third)

然后调用者必须创建一个新的堆对象来保存Int32值 4,第二个来保存值 6,第三个也保存值 6,然后必须将对这些对象的引用传递给CountUniqueThings方法。恶心。

堆对象的创建是有一定成本的。如果将某些东西设为值类型可以避免创建堆对象来保存绝大多数实例的需要,那将是一个巨大的胜利。但是,如果创建的每个实例都需要创建一个堆对象来保存一份副本(例如,分配给接口类型的变量),那么值类型的优势将完全消失。如果实例平均需要创建多个堆对象来保存副本(每次将类型从堆类型转换为值类型并返回时,都需要一个新实例),那么使用值类型可能要少得多比简单地构造一个代码可以传递引用的类类型实例更有效。

于 2013-05-30T20:05:38.270 回答