Подтвердить что ты не робот

Может ли неизменный тип изменить свое внутреннее состояние?

Вопрос прост. Может ли тип, который может изменить свое внутреннее состояние, не будучи видимым снаружи, считается непреложным?

Упрощенный пример:

public struct Matrix
{
    bool determinantEvaluated;
    double determinant;

    public double Determinant 
    {
         get //asume thread-safe correctness in implementation of the getter
         {
             if (!determinantEvaluated)
             {
                  determinant = getDeterminant(this);
                  determinantEvaluated = true;
             }

             return determinant;    
         }
    }
}

UPDATE: Уточнен вопрос безопасности потока, поскольку это вызывало отвлечение.

4b9b3361

Ответ 1

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

В вашем случае возможная реализация выглядит примерно так:

  public class Matrix {
    ...
    private Lazy<Double> m_Determinant = new Lazy<Double>(() => {
      return ... //TODO: Put actual implementation here
    });

    public Double Determinant {
      get {
        return m_Determinant.Value;
      }
    }
  }

Обратите внимание, что Lazy<Double> m_Determinant имеет изменяющееся состояние

m_Determinant.IsValueCreated 

который, однако, ненаблюдаем.

Ответ 2

Это зависит.

Если вы документируете авторов кода клиента или аргументации как автора клиентского кода, то вас интересует интерфейс компонента (то есть его внешнее наблюдаемое состояние и поведение), а не его детали реализации (например, внутреннее представление).

В этом смысле тип неизменен, даже если он кэширует состояние, даже если он инициализируется лениво и т.д. - пока эти мутации не наблюдаются извне. Другими словами, тип является неизменным, если он ведет себя как неизменный при использовании через свой публичный интерфейс (или его другие предполагаемые варианты использования, если они есть).

Конечно, это может быть сложно сделать правильно (с изменчивым внутренним состоянием, вам, возможно, придется заботиться о безопасности потоков, сериализации/маршалинге и т.д.), Но предполагая, что вы все правильно понимаете (по крайней мере, вам нужно), нет никаких оснований не считать такой тип неизменным.

Очевидно, что с точки зрения компилятора или оптимизатора такой тип обычно не считается неизменным (если компилятор не является достаточно интеллектуальным или имеет некоторую "помощь", например, подсказки или предшествующие знания некоторых типов) и любые оптимизации которые были предназначены для неизменяемых типов, могут быть неприменимы, если это так.

Ответ 3

Я собираюсь quote Clojure автор Rich Hickey здесь:

Если дерево падает в лесу, это делает звук?

Если чистая функция мутирует некоторые локальные данные для получения неизменяемого возвращаемого значения, то это нормально?

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

В некотором смысле в сборках мусора, таких как С#, все объекты имеют некоторое состояние из-за GC. Как потребитель, который обычно не касается вас.

Ответ 4

Я выдержу свою шею...

Нет, неизменяемый объект не может изменить свое внутреннее состояние на С#, потому что наблюдение за его памятью является опцией, и поэтому вы можете наблюдать неинициализированное состояние. Доказательство:

public struct Matrix
{
    private bool determinantEvaluated;
    private double determinant;

    public double Determinant
    {
        get
        {
            if (!determinantEvaluated)
            {
                determinant = 1.0;
                determinantEvaluated = true;
            }

            return determinant;
        }
    }
}

затем...

public class Example
{
    public static void Main()
    {
        var unobserved = new Matrix();
        var observed = new Matrix();

        Console.WriteLine(observed.Determinant);

        IntPtr unobservedPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof (Matrix)));
        IntPtr observedPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Matrix)));

        byte[] unobservedMemory = new byte[Marshal.SizeOf(typeof (Matrix))];
        byte[] observedMemory = new byte[Marshal.SizeOf(typeof (Matrix))];

        Marshal.StructureToPtr(unobserved, unobservedPtr, false);
        Marshal.StructureToPtr(observed, observedPtr, false);



        Marshal.Copy(unobservedPtr, unobservedMemory, 0, Marshal.SizeOf(typeof (Matrix)));
        Marshal.Copy(observedPtr, observedMemory, 0, Marshal.SizeOf(typeof (Matrix)));

        Marshal.FreeHGlobal(unobservedPtr);
        Marshal.FreeHGlobal(observedPtr);

        for (int i = 0; i < unobservedMemory.Length; i++)
        {
            if (unobservedMemory[i] != observedMemory[i])
            {
                Console.WriteLine("Not the same");
                return;
            }
        }

        Console.WriteLine("The same");
    }
}

Ответ 5

Целью указания неизменяемого типа является установление следующего инварианта:

  • Если два экземпляра неизменяемого типа когда-либо наблюдаются равными, любая общедоступная ссылка на одну может быть заменена ссылкой на другую, не влияя на поведение.

Поскольку .NET предоставляет возможность сравнивать любые две ссылки для равенства, невозможно достичь идеальной эквивалентности между неизменяемыми экземплярами. Тем не менее, вышеупомянутый инвариант по-прежнему очень полезен, если рассматривать проверки ссылочного равенства как находящиеся вне сферы вещей, для которых ответственен объект класса.

Обратите внимание, что под этим правилом подкласс может определять поля, отличные от тех, которые включены в неизменный базовый класс, но не должны подвергать их таким образом, чтобы нарушать вышеуказанный инвариант. Кроме того, класс может включать изменяемые поля при условии, что они никогда не меняются каким-либо образом, что влияет на состояние класса. Рассмотрим что-то вроде поля hash в классе Java string. Если он отличен от нуля, значение hashCode строки равно значению, хранящемуся в поле. Если он равен нулю, значение hashCode строки является результатом выполнения определенных вычислений в неизменяемой последовательности символов, инкапсулированной строкой. Сохранение результата вышеупомянутых вычислений в поле hash не повлияет на хэш-код строки; это просто ускорит повторные запросы для значения.