14

我一直在使用结构作为隐式验证复杂值对象的机制,以及围绕更复杂类的通用结构来确保有效值。我对性能后果有点无知,所以我希望大家能帮助我。例如,如果我要执行诸如将域对象注入值类型包装器之类的操作,那会导致问题吗?为什么?我了解值类型和引用类型之间的区别,我的目标是利用值类型的不同行为。为了负责任地做到这一点,我究竟需要调查什么?

这是我正在考虑的一个非常基本的想法。

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;
    }
}
4

6 回答 6

11

好吧,一件令人讨厌的事情是,它的行为不像您可能天真地期望的那样:

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

Foo foo1 = wrapper1;
Foo foo2 = wrapper2;

这将创建两个实例,Foo因为在创建实例之前复制了原始版本wrapper1

基本上,您正在处理一个可变结构 - 这几乎从来都不是一件好事。此外,我通常不热衷于隐式转换。

感觉就像您正在尝试在这里实现看起来很神奇的代码……而我通常反对这种事情。也许这对您的特定用例有意义,但我想不出我个人想在哪里使用它。

于 2010-12-09T15:35:30.210 回答
7

正如 Jon 正确指出的那样,这里的问题是该类型的行为是出乎意料的,而不是它很。从性能的角度来看,围绕引用的结构包装器的开销应该非常低。

如果您要做的是表示不可为空的引用类型,那么结构是一种合理的方法;但是,我倾向于通过失去“自动创建”功能来使结构不可变:

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; 
    } 
}

让调用者负责提供有效的参考;如果他们想“新”一个,让他们。

另请注意,通用转换运算符可能会给您带来意想不到的结果。您应该阅读有关转换运算符的规范并彻底理解它。例如,您不能在“对象”周围创建一个非空包装器,然后让该东西隐式转换为展开转换;每个到对象的隐式转换都将是结构上的装箱转换。您不能“替换”C# 语言的内置转换。

于 2010-12-09T17:42:53.523 回答
2

主要的惩罚是对结构进行装箱。它们也是按值传递的,因此在传递给方法时必须复制大型结构:

MyStruct st;
foo.Bar(st); // st is copied
于 2010-12-09T15:34:09.587 回答
2

好的,只是上面的注释。

MyStruct st; foo.Bar(st); // st 被复制

这不是装箱,除非 Bar 的参数是对象,例如。

void Bar(MyStruct parameter){}

不会将值类型装箱。

默认情况下,参数在 c# 中按值传递,除非您使用 ref 或 out 关键字。按值传递的参数被复制。传递结构和对象的区别在于传递的是什么。使用值类型,实际值被复制进来,这意味着创建了一个新的值类型,因此您最终得到了一个副本。使用引用类型,对引用类型的引用被传入。我猜名字中的线索:)

因此,结构的性能会受到影响,因为除非您使用 ref/out 关键字,否则整个结构都会被复制,并且如果您正在广泛地这样做,我认为您的代码需要查看。

装箱是将值类型分配给引用类型变量的过程。创建了一个新的引用类型(对象),并将值类型的副本分配给它。

我有点明白你在原始代码中所做的事情,但它似乎是在用一个具有许多隐式而不是显式复杂性的问题来解决一个简单的问题。

于 2010-12-09T15:56:05.913 回答
2

这个问题的答案似乎不再讨论性能,而是解决可变值类型的危险。

以防万一您发现这很有用,这是我拼凑在一起的一个实现,它使用不可变值类型包装器执行类似于您的原始示例的操作。

不同之处在于我的值类型不直接引用它所引用的对象;相反,它拥有一个键和对使用键 (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
    }
}
于 2010-12-10T16:14:19.167 回答
1

将结构放入集合时会出现另一个性能问题。例如,假设您有一个List<SomeStruct>,并且您想要修改Prop1列表中第一项的属性。最初的倾向是这样写:

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

那不会编译。为了完成这项工作,您必须编写:

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

这会导致两个问题(主要是)。首先,您最终将整个结构复制了两次:一次复制到您的工作myThing实例中,然后返回到列表中。第二个问题是您不能在 a 中执行此操作,foreach因为它会更改集合并将导致枚举器抛出异常。

顺便说一句,你NeverNull的东西有一个相当奇怪的行为。可以将Reference属性设置为null. 这句话让我觉得非常奇怪:

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

已验证。

我很想知道您尝试创建这种类型的结构的原因。

于 2010-12-09T16:01:31.610 回答