访问C#类中的变量是否从内存中读取整个类?

Sag*_*Ziv 2 c# memory oop class memory-address

我对C#相当陌生,我有一个问题困扰了我一段时间。

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

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

例如,如果我有这个课:

public class Test
{
    public int toAccess; // 32 bit
    private byte someValue; // 8 bit
    private short anotherValue; // 16 bit
} 
Run Code Online (Sandbox Code Playgroud)

然后从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
    }
}
Run Code Online (Sandbox Code Playgroud)

我的问题是:确实如此吗?访问变量时是否读取了整个类?

Mar*_*ell 8

对于,这实际上没有任何区别;您总是只处理参考和该参考的偏移量。通过参考很便宜。

当它确实开始关系与结构。注意,这不会影响该类型的调用方法 -通常是基于ref的静态调用;但是当struct 方法的参数时,它很重要。

(编辑:实际上,如果您通过装箱操作来调用结构体上的方法,这也很重要,因为包装盒也是复制品;这是避免装箱调用的重要原因!)

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

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

struct MyBigStruct {
   // lots of fields here
}

void Foo() {
    MyBigStruct x = ...
    Bar(x);
}
void Bar(MyBigStruct s) {...}
Run Code Online (Sandbox Code Playgroud)

然后在调用时Bar(x)结构复制到堆栈上。同样,只要将本地用于存储(假设编译器未将其煮沸):

MyBigStruct x = ...
MyBigStruct asCopy = x;
Run Code Online (Sandbox Code Playgroud)

但!我们可以通过传递引用代替来解决这些问题。在C#的最新版本,这是最适合做使用inref readonly以及readonly 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) {...}
Run Code Online (Sandbox Code Playgroud)

现在实际副本为零。这里的所有内容都在处理对原始文档的引用x。这是事实,这readonly意味着运行时知道它可以信任in参数声明,而无需防御性地复制值。

具有讽刺意味的是,也许:如果输入类型是未标记的,则在in参数上添加修饰符可能会导致复制,因为编译器和运行时需要保证对调用者不可见内部所做的更改。这些变化不必很明显-如果类型是邪恶的,则任何方法调用(包括属性获取器和某些运算符)都可以使值发生变化。举一个邪恶的例子:structreadonlyBar

struct Evil
{
    private int _count;
    public int Count => _count++;
}
Run Code Online (Sandbox Code Playgroud)

即使您是邪恶的,编译器和运行时的工作也是可以预期的,因此它添加了该结构的防御性副本。readonly结构上带有修饰符的相同代码将无法编译


你也可以做类似的东西inref,如果该类型是不是readonly,但是你需要注意,如果Bar发生变异值(故意或副作用),这些变化将是可见的Foo


Oli*_*ier 7

简短答案

没有。

简短答案少

编译器在创建中间语言代码(.NET汇编语言或IL)时创建成员表,并在您访问类成员时在代码中指示要添加到引用的确切偏移量(实例的内存基址) )。

例如(简化形式),如果对象实例的引用位于内存地址0x12345600,而成员的偏移量int Value为0x00000010,则CLR将获得一条指令以执行以获取区域中的内容。 0x12345610。

因此,不需要解析内存中的整个类结构。

长答案

这是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
Run Code Online (Sandbox Code Playgroud)

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

IL_000d: ldfld class ConsoleApp.Program/Test ConsoleApp.Program/MainClass::test
Run Code Online (Sandbox Code Playgroud)

=>在这里它加载test实例的基本内存地址(引用是一个隐藏的指针,忘记了对其进行管理)

IL_0012: ldfld int32 ConsoleApp.Program/Test::toAccess
Run Code Online (Sandbox Code Playgroud)

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

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

在WriteLine中,您将获得以下指令来获取参数值:

ldarg.1
Run Code Online (Sandbox Code Playgroud)