在 C# class 中访问变量是否会从内存中读取整个 class?

Does accessing a variable in C# class reads the entire class from memory?

我是 c# 的新手,有一个问题困扰了我一段时间。

当我学习 C# 时,我被告知 class 不应该包含很多变量,因为这样读取变量(或从中调用方法)会很慢。

有人告诉我,当我在 class C# 中访问一个变量时,它会从内存中读取整个 class 以读取变量数据,但这对我来说听起来很奇怪和错误。

例如,如果我有这个 class:

public class Test
{
    public int toAccess; // 32 bit
    private byte someValue; // 8 bit
    private short anotherValue; // 16 bit
} 

然后从 main 访问它:

public class MainClass
{
    private Test test;
    public MainClass(Test test)
    {
        this.test = test;
    }
    public static void Main(string[] args)
    {
        var main = new MainClass(new Test());
        Console.WriteLine(main.test.toAccess); // Would read all 56 bit of the class
    }
}

我的问题是:这真的是真的吗?访问变量时是否读取整个 class?

简答

没有。

少简答

编译器在创建中间语言代码(.NET 汇编语言或 IL)时创建成员表,并且当您访问 class 成员时,它会在代码中指示要添加到该成员的引用(实例的内存基地址)。

例如(在简化的shorthand中),如果一个对象实例的引用在内存地址0x12345600处,而成员int Value的偏移量是0x00000010,那么CLR将获得一条指令来执行以获取 0x12345610 中区域的内容。

所以不需要解析内存中的整个class结构

长答案

这是来自 ILSpy 的 Main 方法的 IL 代码:

// Method begins at RVA 0x2e64
// Code size 30 (0x1e)
.maxstack 1
.locals init (
  [0] class ConsoleApp.Program/MainClass main
)

// (no C# code)
IL_0000: nop
// MainClass mainClass = new MainClass(new Test());
IL_0001: newobj instance void ConsoleApp.Program/Test::.ctor()
IL_0006: newobj instance void ConsoleApp.Program/MainClass::.ctor(class ConsoleApp.Program/Test)
IL_000b: stloc.0
// Console.WriteLine(mainClass.test.toAccess);
IL_000c: ldloc.0
IL_000d: ldfld class ConsoleApp.Program/Test ConsoleApp.Program/MainClass::test
IL_0012: ldfld int32 ConsoleApp.Program/Test::toAccess
IL_0017: call void [mscorlib]System.Console::WriteLine(int32)
// (no C# code)
IL_001c: nop
// }
IL_001d: ret

如您所见,WriteLine 指令获取要写入的值使用:

IL_000d: ldfld class ConsoleApp.Program/Test ConsoleApp.Program/MainClass::test

=>这里加载test实例的基内存地址(引用是一个隐藏的指针忘记管理它)

IL_0012: ldfld int32 ConsoleApp.Program/Test::toAccess

=> 这里加载toAccess字段的内存地址偏移量。

接下来,通过传递 base + offset Int32 内存区域的内容所需的参数来调用 WriteLine:值被压入堆栈 (ldfld),被调用的方法将弹出此堆栈获取参数 (ldarg)。

在 WriteLine 中,您将有这条指令来获取参数值:

ldarg.1

对于,这实际上没有任何区别;您总是只处理一个引用和该引用的偏移量。传递参考是非常便宜的。

开始变得重要时 结构。请注意,这不会影响 类型上的调用方法 - 通常是基于引用的静态调用;但是当结构是方法的参数时,它很重要。

(编辑:实际上,在结构上调用方法时也很重要 if 你是通过装箱操作调用它们的,因为盒子也是一个副本;这很好避免盒装电话的原因!)

免责声明:您可能不应该经常使用结构。

对于结构,值在任何地方作为值space 占用那么多,它可以作为一个字段,堆栈上的局部变量,一个参数 到方法等。这也意味着 复制 结构(例如,作为参数传递)可能很昂贵。但是,如果我们举个例子:

struct MyBigStruct {
   // lots of fields here
}

void Foo() {
    MyBigStruct x = ...
    Bar(x);
}
void Bar(MyBigStruct s) {...}

然后在调用Bar(x) 时,我们复制 堆栈上的结构。同样,每当本地用于存储时(假设它没有被编译器煮掉):

MyBigStruct x = ...
MyBigStruct asCopy = x;

但是!我们可以解决这些问题...通过传递 reference 来代替。在当前的 C# 版本中,最适合使用 inref readonlyreadonly struct:

readonly struct MyBigStruct {
   // lots of readonly fields here
}
void Foo() {
    MyBigStruct x = ...
    Bar(x); // note that "in" is implicit when needed, unlike "ref" or "out"
    ref readonly MyBigStruct asRef = ref x;
}
void Bar(in MyBigStruct s) {...}

现在有 个实际副本。这里的一切都在处理对原始 x 的引用。它是 readonly 的事实意味着运行时知道它可以信任参数上的 in 声明,而不需要值的防御性副本。

具有讽刺意味的是,也许:如果输入类型是 struct 且未标记 readonly,因为编译器和运行时需要保证在 Bar 中所做的更改对调用者不可见。这些变化不需要很明显——任何方法调用(包括 属性 getter 和一些运算符)都可以改变值,如果类型是邪恶的。作为一个邪恶的例子:

struct Evil
{
    private int _count;
    public int Count => _count++;
}

编译器和运行时的工作是可预测地工作即使你是邪恶的,因此它添加了结构的防御副本。在结构 上使用 readonly 修饰符的相同代码将无法编译 .


如果类型不是 readonly,您也可以使用 ref 执行类似于 in 的操作,但是您需要注意如果 Bar 发生变异值(故意或作为副作用),这些更改将对 Foo.

可见