我对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
}
}
我的问题是:它是真的吗?访问变量时会读取整个类吗?
答案 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#中,最合适的做法是使用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) {...}
现在有 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