среда, 10 декабря 2008 г.

Баг со структурами и readonly

Сегодня столкнулся с любобытнейшим багом в C#. Вполне возможно, что это не баг, а предусмотренное поведение, но выглядит оно необычно.
В двух словах: readonly переменная изменяема только в конструкторе класса и даже если конструктор вызовет метод, изменяющий ее - эти изменения будут отброшены.


Рассмотрим такой код с вспомогательной структурой и классом.

public struct TestStruct
{
private int m_count;
private int m_value;

public int Count
{
get { return m_count; }
}

public int Value
{
get { return m_value; }
}

public TestStruct(int value)
{
m_value = value;
m_count = 0;
}

public void Increment()
{
m_count++;
}
}

public class TestClass
{
private TestStruct m_struct;

public int Value
{
get { return m_struct.Value; }
}

public int Count
{
get { return m_struct.Count; }
}

public TestClass(int value)
{
m_struct = new TestStruct(value);

for (int i = 0; i < value; i++)
Increment();
}

private void Increment()
{
m_struct.Increment();
}
}


Если создать экземпляр TestClass с каким-либо числом, то значения Value и Count будут равны этому числу. Вполне нормальное и логичное поведение.
Картина меняется, если мы добавим слово readonly:

private readonly TestStruct m_struct;


После этого изменения код

TestClass test = new TestClass(11);
Console.WriteLine("Test result: value {0}, count {1}",
test.Value, test.Count);

выдает такой результат:

Test result: value 11, count 0

Верно такое поведение или нет?
Мы знаем, что структуры являются value-type и для каждого члена структуры, вложенной в класс, память выделяется в самом классе. Потому логично предположить, что readonly распостраняется и на члены структуры. Неприятно, но компилятор нам об этом не сообщает и никак не предупреждает, что этот модификатор приведет к потере данных.
Более того, логично было бы предположить, что если метод Increment() обьявлен как private и используется только в конструкторе, то метод будет inline и на него будут распостранятся те же правила, что и для конструктора. К сожалению, это предположение не оправдывается и нам просто надо помнить, что вызов методов в конструкторе может привести к "необычным" последствиям, не говоря о том, что может быть при вызове виртуальных методов.