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

时间:2019-10-09 15:02:42

标签: c# class oop memory memory-address

我对C#还是很陌生,我有一个问题困扰了我一会儿。

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

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

例如,如果我有此类:

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
    }
}

我的问题是:它是真的吗?访问变量时会读取整个类吗?

2 个答案:

答案 0 :(得分:8)

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

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

(edit:实际上,当您通过装箱操作调用结构 if 上的方法时,这也很重要,因为包装盒也是复制品;这是避免装箱调用的重要原因!)

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

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

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) {...}

现在有 zero 个实际副本。这里的所有内容都涉及对原始x的引用。它是readonly的事实意味着运行时知道它可以信任参数上的in声明,而无需该值的防御性副本。

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

Bar

即使您是邪恶的,编译器和运行时的工作是可以预期地工作,因此它会添加结构的防御性副本。带有<{>}结构上的struct Evil { private int _count; public int Count => _count++; } 修饰符的相同代码将无法编译。


如果类型不是readonly,您还可以使用in进行类似于ref的操作,但是您需要知道,如果readonly会改变值(故意或有副作用),这些更改将对Bar可见。

答案 1 :(得分: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

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

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

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

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

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

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

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

ldarg.1