Как предотвратить повреждение внутреннего состояния класса арифметическим переполнением?
У меня в классе два
int
поля и, а также метод, который увеличивает оба этих поля на
dx
а также
dy
соответственно. Я хотел бы предотвратить повреждение состояния моего класса из-за молчаливого арифметического переполнения (что привело бы к отрицательному или отрицательному результату), поэтому я явно увеличиваю поля в блокировать:
class MyClass
{
private int x;
private int y;
public void Increment(int dx, int dy)
{
checked { x += dx; y += dy; }
}
}
Это должно гарантировать, что в случае арифметического переполнения вызывающий получит OverflowException
, и состояние моего класса останется неизменным. Но потом я понял, что арифметическое переполнение могло произойти при приращении
y
, после того, как уже успешно увеличился, что привело к другому типу повреждения состояния, которое не менее разрушительно, чем первое. Поэтому я изменил реализацию метода следующим образом:
public void Increment2(int dx, int dy)
{
int x2, y2;
checked { x2 = x + dx; y2 = y + dy; }
x = x2; y = y2;
}
Это кажется логическим решением проблемы, но теперь я обеспокоен тем, что компилятор может «оптимизировать» мою тщательно созданную реализацию и переупорядочить инструкции таким образом, чтобы позволить
x
назначение должно произойти до
y + dy
Кроме того, в результате снова к государственной коррупции. Я хотел бы спросить, возможен ли, согласно спецификации C #, этот нежелательный сценарий.
Я также рассматриваю возможность удаления
checked
ключевое слово, и вместо этого компилируя мой проект с включенной опцией «Проверить арифметическое переполнение» (
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
). Может ли это иметь какое-либо значение в отношении возможного изменения порядка инструкций внутри
Increment
метод?
Обновление: с помощью деконструкции кортежа возможна более лаконичная реализация, защищенная от переполнения арифметических операций. Эта версия отличается (менее безопасна) от подробной реализации?
public void Increment3(int dx, int dy)
{
(x, y) = checked((x + dx, y + dy));
}
Разъяснение:MyClass
предназначен для использования в однопоточном приложении. Безопасность потоков не вызывает беспокойства (я знаю, что это небезопасно, но это не имеет значения).
3 ответа
Сделайте свой класс неизменным. Если вы хотите что-то изменить, верните новый экземпляр.
class MyClass
{
private int x;
private int y;
public MyClass Increment(int dx, int dy)
{
checked
{
return new MyClass { x = this.x + dx, y = this.y + dy };
}
}
}
И в своем коде вызова вы должны заменить
myClass.Increment( a, b );
с участием
myClass = myClass.Increment( a, b );
Это гарантирует, что ваш класс всегда внутренне непротиворечив.
Если вы не хотите возвращать новый экземпляр, вы можете получить то же преимущество, используя внутреннюю структуру только для чтения.
public readonly struct Coords
{
public int X { get; init; }
public int Y { get; init; }
}
class MyClass
{
private Coords _coords;
public void Increment(int dx, int dy)
{
checked
{
var newValue = new Coords { X = _coords.X + dx, Y = _coords.Y + dy };
}
_coords = newValue;
}
}
TL;DR; Это совершенно безопасно делать в одном потоке.
CLI, который реализует ваш код на собственном машинном языке, просто не разрешается переупорядочивать инструкции таким образом, чтобы иметь видимый побочный эффект, по крайней мере, в том, что касается наблюдений из одного потока. Это запрещено спецификацией.
Давайте посмотрим на ECMA-335 , спецификацию CLR и CLI (выделено жирным шрифтом)
I.12.6.4 Оптимизация
Соответствующие реализации CLI могут свободно выполнять программы с использованием любой технологии, которая гарантирует, что в рамках одного потока выполнения побочные эффекты и исключения, сгенерированные потоком, будут видны в порядке, указанном CIL.
... snip ...
Нет никаких гарантий упорядочения относительно исключений, введенных в поток другим потоком (такие исключения иногда называют «асинхронными исключениями» (например,). [Обоснование: оптимизирующий компилятор может переупорядочивать побочные эффекты и синхронные исключения до такой степени, что это переупорядочение не меняет наблюдаемого поведения программы . конец обоснования ]
[ Примечание. В реализации CLI разрешается использовать оптимизирующий компилятор, например, для преобразования CIL в машинный код, при условии, что компилятор поддерживает (в каждом отдельном потоке выполнения) тот же порядок побочных эффектов и синхронных исключений .
Это более сильное условие, чем ISO C++ (который разрешает переупорядочивание между парой точек последовательности) или ISO Scheme (который позволяет переупорядочивать аргументы функций). конец примечания]
Таким образом, исключения должны происходить в порядке, указанном в коде IL, скомпилированном C #, поэтому, если происходит переполнение в
Обратите внимание, что это не означает, что два добавления не могут произойти до сохранения в локальные переменные, потому что локальные переменные не могут быть обнаружены после того, как возникло исключение. В полностью оптимизированной сборке локальные переменные, вероятно, будут храниться в регистрах ЦП и стираться в случае исключения.
Процессор также может изменять порядок внутри, если применяются те же гарантии.
Из всего этого есть одно исключение, за исключением упомянутого допуска многопоточности:
Оптимизаторам предоставляется дополнительная свобода для упрощенных исключений в методах. Метод E- ослаблен для своего рода исключения, если самый внутренний настраиваемый атрибут
относящийся к исключениям типа E присутствует и указывает, что исключения типа E смягчаются.
Однако текущая реализация Microsoft в любом случае не предоставляет такой возможности ослабления.
Что касается использования синтаксиса деконструкции кортежей, к сожалению, спецификация для C# 7 не была выпущена, но эта страница в Github указывает, что она также должна быть без побочных эффектов.
Если вы хотите предотвратить переупорядочение, следует использовать метод MemoryBarrier.
используйте эту ссылку:https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.memorybarrier?redirectedfrom=MSDN&amp;view=net-5.0#System_Threading_Thread_MemoryBarrier