Почему Nullable <T>является структурой? - PullRequest
23 голосов
/ 25 ноября 2010

Мне было интересно, почему Nullable<T> является типом значения, если он предназначен для имитации поведения ссылочных типов?Я понимаю такие вещи, как давление GC, но я не чувствую себя уверенным - если мы хотим, чтобы int действовал как ссылка, мы, вероятно, в порядке со всеми последствиями наличия реального ссылочного типа.Я не вижу причин, по которым Nullable<T> - это не просто коробочная версия T struct.

Как тип значения:

  1. , его по-прежнему нужно упаковывать и распаковывать, и многое другое, бокс должен немного отличаться от «нормальных» структур (для обработки null-значимых обнуляемых как реальных null)
  2. его нужно обрабатывать по-разному при проверке на null (сделанопросто в Equals, без особых проблем)
  3. это изменчиво, нарушая правило, согласно которому структуры должны быть неизменяемыми (хорошо, это логически неизменяемо)
  4. это нужноиметь специальное ограничение для запрета рекурсии, например Nullable<Nullable<T>>

Не делает ли Nullable<T> ссылочный тип решающим эти проблемы?

перефразировано и обновлено:

Я немного изменил свой список причин, но мой общий вопрос все еще открыт:

Как ссылочный тип Nullable<T> будет хуже, чем реализация типа текущего значения?Это только давление в GC и «маленькое, неизменное» правило?Мне все еще странно ...

Ответы [ 4 ]

16 голосов
/ 28 ноября 2010

Причина в том, что это , а не , предназначенное для работы в качестве ссылочного типа. Он был разработан, чтобы действовать как тип значения, за исключением только одного конкретного. Давайте рассмотрим некоторые различия между типами значений и ссылочными типами.

Основное различие между значением и ссылочным типом состоит в том, что тип значения является автономным (переменная, содержащая фактическое значение), тогда как ссылочный тип ссылается на другое значение.

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

Тип значения имеет концепцию равенства, основанную на содержащемся значении, которое может быть необязательно переопределено (существуют логические ограничения того, как это переопределение может происходить *). У ссылочного типа есть концепция тождества, которая не имеет смысла для типов значений (поскольку они не могут иметь прямой псевдоним, поэтому два таких значения не могут быть идентичными), которые не могут быть переопределены, что также дает значение по умолчанию для его концепции равенства. По умолчанию == имеет дело с этим равенством на основе значений, когда речь идет о типах значений †, но с тождеством, когда речь идет о ссылочных типах. Кроме того, даже когда ссылочному типу дается основанная на значении концепция равенства и он используется для ==, он никогда не теряет возможности сравниваться с другой ссылкой на идентичность.

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

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

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

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

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


Теперь Nullable<T> - это тип, который ведет себя как тип значения всеми способами, описанными выше, за исключением того, что он может принимать нулевое значение. Может быть, вопрос о локальных значениях, хранящихся в стеке, не так уж важен (будучи скорее деталью реализации, чем чем-либо еще), но остальное присуще тому, как оно определено.

Nullable<T> определяется как

struct Nullable<T>
{
    private bool hasValue;
    internal T value;
    /* methods and properties I won't go into here */
}

Большая часть реализации с этой точки зрения очевидна. Требуется некоторая специальная обработка, позволяющая присваивать ему значение null - обрабатывается так, как если бы был назначен default(Nullable<T>) - и некоторая специальная обработка в штучной упаковке, а затем следует остальное (включая то, что его можно сравнивать на равенство с нулем).

Если бы Nullable<T> был ссылочным типом, то нам потребовалась бы специальная обработка, чтобы все остальное происходило, наряду со специальной обработкой функций того, как .NET помогает разработчику (например, нам нужны специальныеобработка, чтобы заставить его спуститься с ValueType).Я даже не уверен, возможно ли это.

* Существуют некоторые ограничения на то, как нам разрешено переопределять равенство.Комбинируя эти правила с теми, которые используются в значениях по умолчанию, тогда, как правило, мы можем допустить, чтобы два значения считались равными, что по умолчанию считается неравным, но редко имеет смысл считать два значения неравными, которые по умолчанию будут считаться равными.Исключением является случай, когда структура содержит только типы значений, но когда указанные типы значений переопределяют равенство.Это результат оптимизации, и, как правило, считается ошибкой, а не дизайном.

† Исключение составляют типы с плавающей точкой.Из-за определения типов значений в стандарте CLI, double.NaN.Equals(double.NaN) и float.NaN.Equals(float.NaN) возвращают true.Но из-за определения NaN в ISO 60559, float.NaN == float.NaN и double.NaN == double.NaN оба возвращают false.

9 голосов
/ 25 ноября 2010

Отредактировано для решения обновленного вопроса ...

Вы можете блокировать и распаковывать объекты, если хотите использовать структуру в качестве ссылки.

Однако тип Nullable<> в основном позволяет расширить любой тип значения с помощью дополнительного флага состояния, который указывает, должно ли значение использоваться как null или является ли структура действительной.

Итак, чтобы ответить на ваши вопросы:

  1. Это преимущество при использовании в коллекциях или из-за различной семантики (копирование вместо ссылок)

  2. Нет, это не так. CLR учитывает это при упаковке и распаковке, так что вы фактически никогда не будете помещать в экземпляр Nullable<>. Бокс Nullable<>, который "не имеет" никакого значения, вернет ссылку null, а распаковка - наоборот.

  3. Нет.

  4. Опять же, это не так. На самом деле, общие ограничения для структуры не позволяют использовать допустимые для использования структуры. Это имеет смысл из-за особого поведения бокса / распаковки. Поэтому, если у вас есть where T: struct для ограничения универсального типа, типы, допускающие обнуление, будут запрещены. Поскольку это ограничение определено также для типа Nullable<T>, вы не можете их вкладывать без какой-либо специальной обработки, чтобы предотвратить это.

Почему бы не использовать ссылки? Я уже упоминал важные смысловые различия. Но помимо этого ссылочные типы используют гораздо больше памяти: каждая ссылка, особенно в 64-разрядных средах, использует не только кучу памяти для экземпляра, но и память для ссылки, информацию о типе экземпляра, биты блокировки и т. Д. Таким образом, помимо семантики и различий в производительности (косвенное указание через ссылку), вы в конечном итоге используете кратное количество памяти, используемой для самой сущности для большинства общих сущностей. И GC получает больше объектов для обработки, что делает общую производительность по сравнению со структурами еще хуже.

6 голосов
/ 25 ноября 2010

Это не изменчиво; проверьте еще раз.

Бокс тоже другой; пустые «ящики» в ноль.

Но; он маленький (чуть больше T), неизменный и заключает в себе только структуры - идеальные как структуры Возможно, что еще более важно, если T действительно является «значением», то и T тоже? логическое «значение».

0 голосов
/ 28 марта 2018

Я закодировал MyNullable как класс. Не могу понять, почему это не может быть класс, кроме того, чтобы избежать нагрузки памяти кучи.

namespace ClassLibrary1

{ используя NFluent;

using NUnit.Framework;

[TestFixture]
class MyNullableShould
{
    [Test]
    public void operator_equals_btw_nullable_and_value_works()
    {
        var myNullable = new MyNullable<int>(1);

        Check.That(myNullable == 1).IsEqualTo(true);
        Check.That(myNullable == 2).IsEqualTo(false);
    }

    [Test]
    public void Can_be_comparedi_with_operator_equal_equals()
    {
        var myNullable = new MyNullable<int>(1);
        var myNullable2 = new MyNullable<int>(1);

        Check.That(myNullable == myNullable2).IsTrue();
        Check.That(myNullable == myNullable2).IsTrue();

        var myNullable3 = new MyNullable<int>(2);
        Check.That(myNullable == myNullable3).IsFalse();
    }
}

} пространство имен ClassLibrary1 { использование системы;

public class MyNullable<T> where T : struct
{
    internal T value;

    public MyNullable(T value)
    {
        this.value = value;
        this.HasValue = true;
    }

    public bool HasValue { get; }

    public T Value
    {
        get
        {
            if (!this.HasValue) throw new Exception("Cannot grab value when has no value");
            return this.value;
        }
    }

    public static explicit operator T(MyNullable<T> value)
    {
        return value.Value;
    }

    public static implicit operator MyNullable<T>(T value)
    {
        return new MyNullable<T>(value);
    }

    public static bool operator ==(MyNullable<T> n1, MyNullable<T> n2)
    {
        if (!n1.HasValue) return !n2.HasValue;
        if (!n2.HasValue) return false;
        return Equals(n1.value, n2.value);
    }

    public static bool operator !=(MyNullable<T> n1, MyNullable<T> n2)
    {
        return !(n1 == n2);
    }

    public override bool Equals(object other)
    {
        if (!this.HasValue) return other == null;
        if (other == null) return false;
        return this.value.Equals(other);
    }

    public override int GetHashCode()
    {
        return this.HasValue ? this.value.GetHashCode() : 0;
    }

    public T GetValueOrDefault()
    {
        return this.value;
    }

    public T GetValueOrDefault(T defaultValue)
    {
        return this.HasValue ? this.value : defaultValue;
    }

    public override string ToString()
    {
        return this.HasValue ? this.value.ToString() : string.Empty;
    }
}

}

...