Читает ли доступ к переменной в классе C # весь класс из памяти? - PullRequest
2 голосов
/ 09 октября 2019

Я довольно новичок в c #, и у меня есть один вопрос, который меня беспокоит некоторое время.

Когда я выучил C #, меня учили, что класс не должен содержать много переменных, потому что затем читает переменную(или вызов метода из него) будет медленным.

Мне сказали, что когда я обращаюсь к переменной в классе C #, она читает весь класс из памяти, чтобы прочитать данные переменной, но это звучит страннои неправильно для меня.

Например, если у меня есть этот класс:

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

Затем доступ к нему с основного:

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 ]

8 голосов
/ 09 октября 2019

Для классов это буквально не имеет никакого значения;Вы всегда имеете дело со ссылкой и смещением от этой ссылки. Передача ссылки довольно дешево.

Когда она действительно начинает иметь значение с структурами . Обратите внимание, что это не влияет на вызов методов для типа - обычно это статический вызов на основе ref;но когда структура является параметром для метода, это имеет значение.

(edit: фактически, это также имеет значение при вызове методов для структур , если вы вызываетеих с помощью операции бокса, поскольку коробка также является копией; это отличная причина избегать коробочных вызовов!)

Отказ от ответственности: вы вероятно не должны регулярно использовать структуры.

Для структур значение занимает столько места везде, где оно используется в качестве значения , которое может быть в качестве поля, локальным в стеке, параметром дляметод и т. д. Это также означает, что копирование структуры (например, для передачи в качестве параметра) может быть дорогостоящим. Но если мы возьмем пример:

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 для параметра, не нуждаясь в защитной копии значения.

По иронии судьбы, возможно: добавление inмодификатор параметра может вводить копирование, если тип ввода - struct, который не помечен readonly, поскольку компилятор и среда выполнения должны гарантировать, что изменения, сделанные внутри Bar, не будут видныдля звонящего. Эти изменения не должны быть очевидными - любой вызов метода (который включает средства получения свойств и некоторые операторы) может изменить значение, если тип является злым. В качестве злого примера:

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

Работа компилятора и среды выполнения состоит в том, чтобы работать предсказуемо , даже если вы злой, поэтому он добавляет защитную копию структуры. Тот же код с модификатором readonly в структуре не скомпилирует .


Вы также можете сделать что-то похожее на in с ref, если тип isn 't readonly, но тогда вам необходимо знать, что если Bar изменяет значение (намеренно или как побочный эффект), эти изменения будут видны Foo.

7 голосов
/ 09 октября 2019

Краткий ответ

Нет.

Менее короткий ответ

Компилятор создает таблицы членов, когда он создает код промежуточного языка (язык ассемблера .NET или IL) и когда вы обращаетесь к члену класса, онуказывает в коде точное смещение, которое будет добавлено к ссылке (базовому адресу памяти экземпляра) этого члена.

Например (в упрощенном виде), если ссылка на экземпляр объекта равнапо адресу памяти 0x12345600 и смещению члена int Value 0x00000010, затем CLR получит инструкцию для выполнения для извлечения содержимого зоны в 0x12345610.

Так что анализировать не нужновся структура класса в памяти.

Длинный ответ

Вот код IL вашего метода Main из ILSpy:

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

AsВы можете видеть, что инструкция WriteLine получает значение для записи, используя:

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

=> Здесь она загружает базовый адрес памяти экземпляра test (ссылка является скрытой точкой). т. е. забыть управлять им)

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

=> Здесь загружается смещение адреса памяти поля toAccess.

Затем вызывается WriteLine путем передачи необходимого параметра, который является содержимым зоны памяти base + offset Int32: значение помещается в стек (ldfld), и вызываемый метод извлекает этот стек вполучить параметр (ldarg).

В WriteLine у ​​вас будет эта инструкция для получения значения параметра:

ldarg.1
...