Когда использует С# structs (типы значений) жертвует производительность?

Я играл с структурами как механизм для неявной проверки объектов комплексных значений, а также общих структур вокруг более сложных классов для обеспечения допустимых значений. Я немного неосведомлен о последствиях работы, поэтому я надеюсь, что вы все можете мне помочь. Например, если бы я сделал что-то вроде инъекции объекта домена в оболочку типа значения, это могло бы вызвать проблемы? Зачем? Я понимаю разницу между типами значений и ссылочными типами, и моя цель здесь состоит в том, чтобы использовать различное поведение типов значений. Что именно мне нужно для изучения, чтобы сделать это ответственно?

Вот очень основная идея того, о чем я думал.

public struct NeverNull<T>
    where T: class, new()
{

    private NeverNull(T reference)
    {
        _reference = reference;
    }

    private T _reference;

    public T Reference
    {
        get
        {
            if(_reference == null)
            {
                _reference = new T();
            }
            return _reference;
        }
        set
        {
            _reference = value;
        }
    }

    public static implicit operator NeverNull<T>(T reference)
    {
        return new NeverNull<T>(reference);
    }

    public static implicit operator T(NeverNull<T> value)
    {
        return value.Reference;
    }
}

Ответ 1

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

NeverNull<Foo> wrapper1 = new NeverNull<Foo>();
NeverNull<Foo> wrapper2 = wrapper1;

Foo foo1 = wrapper1;
Foo foo2 = wrapper2;

Это приведет к созданию двух экземпляров Foo, потому что исходная версия была скопирована, перед тем как wrapper1 создал экземпляр.

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

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

Ответ 2

Как верно указывает Джон, проблема здесь в том, что поведение типа неожиданно, а не медленное. С точки зрения производительности накладные расходы на обертку структуры вокруг ссылки должны быть очень низкими.

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

public struct NeverNull<T> where T: class 
{ 
    private NeverNull(T reference) : this()
    { 
        if (reference == null) throw new Exception(); // Choose the right exception
        this.Reference = reference; 
    } 

    public T Reference { get; private set; }

    public static implicit operator NeverNull<T>(T reference) 
    { 
        return new NeverNull<T>(reference); 
    } 

    public static implicit operator T(NeverNull<T> value) 
    { 
        return value.Reference; 
    } 
}

Сделать вызывающего абонента ответственным за предоставление действительной ссылки; если они хотят "нового", пусть они.

Обратите также внимание на то, что общие операторы преобразования могут дать вам неожиданные результаты. Вы должны прочитать спецификацию операторов конвертации и понять ее полностью. Например, вы не можете создать ненулевую оболочку вокруг "объекта", а затем эту вещь неявно конвертировать в развернутое преобразование; каждое неявное преобразование в объект будет преобразованием бокса в структуру. Вы не можете "заменить" встроенное преобразование языка С#.

Ответ 3

Основное наказание - бокс для структур. Также они передаются по значению, поэтому большая структура при передаче методу должна быть скопирована:

MyStruct st;
foo.Bar(st); // st is copied

Ответ 4

Хорошо, просто примечание к сказанному выше.

MyStruct st; foo.Bar(ул);//st скопировано

Это не бокс, если параметр Bar не является объектом, например.

void Bar(MyStruct parameter){}

не будет указывать тип значения.

Параметры передаются по значению в С# по умолчанию, если вы не используете ключевое слово ref или out. Скопированы параметры, переданные по значению. Разница между передачей структуры и объектом - это то, что передается. С типом значения фактическое значение копируется в, что означает, что создается новый тип значения, поэтому вы получаете копию. С ссылочным типом передается ссылка на ссылочный тип. Подсказки в имени, который я предполагаю:)

Таким образом, для структур создается удар производительности, потому что вся структура копируется, если вы не используете ключевое слово ref/out, и если вы делаете это широко, я думаю, что ваш код нуждается в поиске.

Бокс - это процесс присвоения типа значения переменной ссылочного типа. Создается новый ссылочный тип (объект) и копия типа значения, назначенного ему.

Я как бы понял, что вы делали в исходном коде, но, похоже, он решает одну простую проблему с тем, что имеет много неявных, а не явных сложностей.

Ответ 5

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

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

Разница в том, что мой тип значения напрямую не ссылается на объект, к которому он относится; вместо этого он содержит ключ и ссылки на делегатов, которые выполняют поиск с помощью ключа (TryGetValueFunc) или создают с помощью ключа. (Примечание: моя первоначальная реализация имела оболочку, содержащую ссылку на объект IDictionary, но я сменил ее на делегат TryGetValueFunc, чтобы сделать ее более гибкой, хотя это может быть более запутанным, и я не уверен на 100%, что это не открыло какой-то недостаток).

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

Ниже приведен полный рабочий пример, а также пример использования для консольной программы:

public delegate bool TryGetValueFunc<TKey, TValue>(TKey key, out TValue value);

public struct KeyedValueWrapper<TKey, TValue>
{
    private bool _KeyHasBeenSet;
    private TKey _Key;
    private TryGetValueFunc<TKey, TValue> _TryGetValue;
    private Func<TKey, TValue> _CreateValue;

    #region Constructors

    public KeyedValueWrapper(TKey key)
    {
        _Key = key;
        _KeyHasBeenSet = true;
        _TryGetValue = null;
        _CreateValue = null;
    }

    public KeyedValueWrapper(TKey key, TryGetValueFunc<TKey, TValue> tryGetValue)
    {
        _Key = key;
        _KeyHasBeenSet = true;
        _TryGetValue = tryGetValue;
        _CreateValue = null;
    }

    public KeyedValueWrapper(TKey key, Func<TKey, TValue> createValue)
    {
        _Key = key;
        _KeyHasBeenSet = true;
        _TryGetValue = null;
        _CreateValue = createValue;
    }

    public KeyedValueWrapper(TKey key, TryGetValueFunc<TKey, TValue> tryGetValue, Func<TKey, TValue> createValue)
    {
        _Key = key;
        _KeyHasBeenSet = true;
        _TryGetValue = tryGetValue;
        _CreateValue = createValue;
    }

    public KeyedValueWrapper(TryGetValueFunc<TKey, TValue> tryGetValue)
    {
        _Key = default(TKey);
        _KeyHasBeenSet = false;
        _TryGetValue = tryGetValue;
        _CreateValue = null;
    }

    public KeyedValueWrapper(TryGetValueFunc<TKey, TValue> tryGetValue, Func<TKey, TValue> createValue)
    {
        _Key = default(TKey);
        _KeyHasBeenSet = false;
        _TryGetValue = tryGetValue;
        _CreateValue = createValue;
    }

    public KeyedValueWrapper(Func<TKey, TValue> createValue)
    {
        _Key = default(TKey);
        _KeyHasBeenSet = false;
        _TryGetValue = null;
        _CreateValue = createValue;
    }

    #endregion

    #region "Change" methods

    public KeyedValueWrapper<TKey, TValue> Change(TKey key)
    {
        return new KeyedValueWrapper<TKey, TValue>(key, _TryGetValue, _CreateValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, TryGetValueFunc<TKey, TValue> tryGetValue)
    {
        return new KeyedValueWrapper<TKey, TValue>(key, tryGetValue, _CreateValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, Func<TKey, TValue> createValue)
    {
        return new KeyedValueWrapper<TKey, TValue>(key, _TryGetValue, createValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, TryGetValueFunc<TKey, TValue> tryGetValue, Func<TKey, TValue> createValue)
    {
        return new KeyedValueWrapper<TKey, TValue>(key, tryGetValue, createValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(TryGetValueFunc<TKey, TValue> tryGetValue)
    {
        return new KeyedValueWrapper<TKey, TValue>(_Key, tryGetValue, _CreateValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(TryGetValueFunc<TKey, TValue> tryGetValue, Func<TKey, TValue> createValue)
    {
        return new KeyedValueWrapper<TKey, TValue>(_Key, tryGetValue, createValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(Func<TKey, TValue> createValue)
    {
        return new KeyedValueWrapper<TKey, TValue>(_Key, _TryGetValue, createValue);
    }

    #endregion

    public TValue Value
    {
        get
        {
            if (!_KeyHasBeenSet)
                throw new InvalidOperationException("A key must be specified.");

            if (_TryGetValue == null)
                throw new InvalidOperationException("A \"try get value\" delegate must be specified.");

            // try to find a value in the given dictionary using the given key
            TValue value;
            if (!_TryGetValue(_Key, out value))
            {
                if (_CreateValue == null)
                    throw new InvalidOperationException("A \"create value\" delegate must be specified.");

                // if not found, create a value
                value = _CreateValue(_Key);
            }
            // then return that value
            return value;
        }
    }
}

class Foo
{
    public string ID { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var dictionary = new Dictionary<string, Foo>();

        Func<string, Foo> createValue = (key) =>
        {
            var foo = new Foo { ID = key };
            dictionary.Add(key, foo);
            return foo;
        };

        // this wrapper object is not useable, since no key has been specified for it yet
        var wrapper = new KeyedValueWrapper<string, Foo>(dictionary.TryGetValue, createValue);

        // create wrapper1 based on the wrapper object but changing the key to "ABC"
        var wrapper1 = wrapper.Change("ABC");
        var wrapper2 = wrapper1;

        Foo foo1 = wrapper1.Value;
        Foo foo2 = wrapper2.Value;

        Console.WriteLine("foo1 and foo2 are equal? {0}", object.ReferenceEquals(foo1, foo2));
        // Output: foo1 and foo2 are equal? True

        // create wrapper1 based on the wrapper object but changing the key to "BCD"
        var wrapper3 = wrapper.Change("BCD");
        var wrapper4 = wrapper3;

        Foo foo3 = wrapper3.Value;
        dictionary = new Dictionary<string, Foo>(); // throw a curve ball by reassigning the dictionary variable
        Foo foo4 = wrapper4.Value;

        Console.WriteLine("foo3 and foo4 are equal? {0}", object.ReferenceEquals(foo3, foo4));
        // Output: foo3 and foo4 are equal? True

        Console.WriteLine("foo1 and foo3 are equal? {0}", object.ReferenceEquals(foo1, foo3));
        // Output: foo1 and foo3 are equal? False
    }
}

Альтернативная реализация с использованием IDictionary<string, Foo> вместо TryGetValueFunc<string, Foo>. Обратите внимание на встречный пример, который я ввел в код использования:

public struct KeyedValueWrapper<TKey, TValue>
{
    private bool _KeyHasBeenSet;
    private TKey _Key;
    private IDictionary<TKey, TValue> _Dictionary;
    private Func<TKey, TValue> _CreateValue;

    #region Constructors

    public KeyedValueWrapper(TKey key)
    {
        _Key = key;
        _KeyHasBeenSet = true;
        _Dictionary = null;
        _CreateValue = null;
    }

    public KeyedValueWrapper(TKey key, IDictionary<TKey, TValue> dictionary)
    {
        _Key = key;
        _KeyHasBeenSet = true;
        _Dictionary = dictionary;
        _CreateValue = null;
    }

    public KeyedValueWrapper(TKey key, Func<TKey, TValue> createValue)
    {
        _Key = key;
        _KeyHasBeenSet = true;
        _Dictionary = null;
        _CreateValue = createValue;
    }

    public KeyedValueWrapper(TKey key, IDictionary<TKey, TValue> dictionary, Func<TKey, TValue> createValue)
    {
        _Key = key;
        _KeyHasBeenSet = true;
        _Dictionary = dictionary;
        _CreateValue = createValue;
    }

    public KeyedValueWrapper(IDictionary<TKey, TValue> dictionary)
    {
        _Key = default(TKey);
        _KeyHasBeenSet = false;
        _Dictionary = dictionary;
        _CreateValue = null;
    }

    public KeyedValueWrapper(IDictionary<TKey, TValue> dictionary, Func<TKey, TValue> createValue)
    {
        _Key = default(TKey);
        _KeyHasBeenSet = false;
        _Dictionary = dictionary;
        _CreateValue = createValue;
    }

    public KeyedValueWrapper(Func<TKey, TValue> createValue)
    {
        _Key = default(TKey);
        _KeyHasBeenSet = false;
        _Dictionary = null;
        _CreateValue = createValue;
    }

    #endregion

    #region "Change" methods

    public KeyedValueWrapper<TKey, TValue> Change(TKey key)
    {
        return new KeyedValueWrapper<TKey, TValue>(key, _Dictionary, _CreateValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, IDictionary<TKey, TValue> dictionary)
    {
        return new KeyedValueWrapper<TKey, TValue>(key, dictionary, _CreateValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, Func<TKey, TValue> createValue)
    {
        return new KeyedValueWrapper<TKey, TValue>(key, _Dictionary, createValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(TKey key, IDictionary<TKey, TValue> dictionary, Func<TKey, TValue> createValue)
    {
        return new KeyedValueWrapper<TKey, TValue>(key, dictionary, createValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(IDictionary<TKey, TValue> dictionary)
    {
        return new KeyedValueWrapper<TKey, TValue>(_Key, dictionary, _CreateValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(IDictionary<TKey, TValue> dictionary, Func<TKey, TValue> createValue)
    {
        return new KeyedValueWrapper<TKey, TValue>(_Key, dictionary, createValue);
    }

    public KeyedValueWrapper<TKey, TValue> Change(Func<TKey, TValue> createValue)
    {
        return new KeyedValueWrapper<TKey, TValue>(_Key, _Dictionary, createValue);
    }

    #endregion

    public TValue Value
    {
        get
        {
            if (!_KeyHasBeenSet)
                throw new InvalidOperationException("A key must be specified.");

            if (_Dictionary == null)
                throw new InvalidOperationException("A dictionary must be specified.");

            // try to find a value in the given dictionary using the given key
            TValue value;
            if (!_Dictionary.TryGetValue(_Key, out value))
            {
                if (_CreateValue == null)
                    throw new InvalidOperationException("A \"create value\" delegate must be specified.");

                // if not found, create a value and add it to the dictionary
                value = _CreateValue(_Key);
                _Dictionary.Add(_Key, value);
            }
            // then return that value
            return value;
        }
    }
}

class Foo
{
    public string ID { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        // this wrapper object is not useable, since no key has been specified for it yet
        var wrapper = new KeyedValueWrapper<string, Foo>(new Dictionary<string, Foo>(), (key) => new Foo { ID = key });

        // create wrapper1 based on the wrapper object but changing the key to "ABC"
        var wrapper1 = wrapper.Change("ABC");
        var wrapper2 = wrapper1;

        Foo foo1 = wrapper1.Value;
        Foo foo2 = wrapper2.Value;

        Console.WriteLine("foo1 and foo2 are equal? {0}", object.ReferenceEquals(foo1, foo2));
        // Output: foo1 and foo2 are equal? True

        // create wrapper1 based on the wrapper object but changing the key to "BCD"
        var wrapper3 = wrapper.Change("BCD");
        var wrapper4 = wrapper3;

        Foo foo3 = wrapper3.Value;
        Foo foo4 = wrapper4.Value;

        Console.WriteLine("foo3 and foo4 are equal? {0}", object.ReferenceEquals(foo3, foo4));
        // Output: foo3 and foo4 are equal? True

        Console.WriteLine("foo1 and foo3 are equal? {0}", object.ReferenceEquals(foo1, foo3));
        // Output: foo1 and foo3 are equal? False


        // Counter-example: manipulating the dictionary instance that was provided to the wrapper can disrupt expected behavior
        var dictionary = new Dictionary<string, Foo>();

        var wrapper5 = wrapper.Change("CDE", dictionary);
        var wrapper6 = wrapper5;

        Foo foo5 = wrapper5.Value;
        dictionary.Clear();
        Foo foo6 = wrapper6.Value;

        // one might expect this to be true:
        Console.WriteLine("foo5 and foo6 are equal? {0}", object.ReferenceEquals(foo5, foo6));
        // Output: foo5 and foo6 are equal? False
    }
}

Ответ 6

Другая проблема с производительностью возникает, когда вы кладете структуры в коллекции. Например, представьте, что у вас есть List<SomeStruct>, и вы хотите изменить свойство Prop1 первого элемента в списке. Начальный наклон заключается в том, чтобы написать это:

List<SomeStruct> MyList = CreateList();
MyList[0].Prop1 = 42;

Это не собирается компилироваться. Для выполнения этой работы вам необходимо написать:

SomeStruct myThing = MyList[0];
myThing.Prop1 = 42;
MyList[0] = myThing.Prop1;

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

Кстати, ваша вещь NeverNull имеет довольно странное поведение. Можно установить свойство Reference в значение null. Мне очень странно, что это утверждение:

var Contradiction = new NeverNull<object>(null);

Действителен.

Мне было бы интересно узнать причины, по которым вы пытаетесь создать этот тип структуры.