在 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# 版本中,最适合使用 in
、ref 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) {...}
现在有零 个实际副本。这里的一切都在处理对原始 x
的引用。它是 readonly
的事实意味着运行时知道它可以信任参数上的 in
声明,而不需要值的防御性副本。
具有讽刺意味的是,也许:如果输入类型是 struct
且未标记 readonly
,因为编译器和运行时需要保证在 Bar
中所做的更改对调用者不可见。这些变化不需要很明显——任何方法调用(包括 属性 getter 和一些运算符)都可以改变值,如果类型是邪恶的。作为一个邪恶的例子:
struct Evil
{
private int _count;
public int Count => _count++;
}
编译器和运行时的工作是可预测地工作即使你是邪恶的,因此它添加了结构的防御副本。在结构 上使用 readonly
修饰符的相同代码将无法编译 .
如果类型不是 readonly
,您也可以使用 ref
执行类似于 in
的操作,但是您需要注意如果 Bar
发生变异值(故意或作为副作用),这些更改将对 Foo
.
可见
我是 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# 版本中,最适合使用 in
、ref 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) {...}
现在有零 个实际副本。这里的一切都在处理对原始 x
的引用。它是 readonly
的事实意味着运行时知道它可以信任参数上的 in
声明,而不需要值的防御性副本。
具有讽刺意味的是,也许:如果输入类型是 struct
且未标记 readonly
,因为编译器和运行时需要保证在 Bar
中所做的更改对调用者不可见。这些变化不需要很明显——任何方法调用(包括 属性 getter 和一些运算符)都可以改变值,如果类型是邪恶的。作为一个邪恶的例子:
struct Evil
{
private int _count;
public int Count => _count++;
}
编译器和运行时的工作是可预测地工作即使你是邪恶的,因此它添加了结构的防御副本。在结构 上使用 readonly
修饰符的相同代码将无法编译 .
如果类型不是 readonly
,您也可以使用 ref
执行类似于 in
的操作,但是您需要注意如果 Bar
发生变异值(故意或作为副作用),这些更改将对 Foo
.