Я довольно новичок в 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
SagiZiv 9 Окт 2019 в 18:02

2 ответа

Лучший ответ

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

Нет.

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

Компилятор создает таблицы членов, когда он создает код промежуточного языка (язык ассемблера .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

Как видите, инструкция 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
8
Olivier Rogier 9 Окт 2019 в 16:38

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

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

(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;

Но! мы можем исправить это ... передав взамен ссылку . В текущих версиях 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, если тип не readonly, но тогда вам нужно знать, что если Bar изменяет значение (намеренно или как побочный эффект) эти изменения будут видны Foo.

8
Marc Gravell 9 Окт 2019 в 21:52