24

所以这就是问题的核心:Foo.Bar 可以返回 null 吗?为了澄清,'_bar'可以在它被评估为非空之后并且在它的值被返回之前设置为空吗?

    public class Foo
    {
        Object _bar;
        public Object Bar
        {
            get { return _bar ?? new Object(); }
            set { _bar = value; }
        }
    }

我知道使用下面的 get 方法是不安全的,并且可以返回一个空值:

            get { return _bar != null ? _bar : new Object(); }

更新:

另一种看待同一问题的方法,这个例子可能更清楚:

        public static T GetValue<T>(ref T value) where T : class, new()
        {
            return value ?? new T();
        }

再次询问 GetValue(...) 可以返回 null 吗?根据您的定义,这可能是线程安全的,也可能不是线程安全的...我想正确的问题陈述是询问它是否是对值的原子操作... David Yaw 最好地定义了这个问题,即上述函数是等价的到以下:

        public static T GetValue<T>(ref T value) where T : class, new()
        {
            T result = value;
            if (result != null)
                return result;
            else
                return new T();
        }
4

4 回答 4

23

不,这不是线程安全的。

上述的 IL 编译为:

.method public hidebysig specialname instance object get_Bar() cil managed
{
    .maxstack 2
    .locals init (
        [0] object CS$1$0000)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldfld object ConsoleApplication1.Program/MainClass::_bar
    L_0007: dup 
    L_0008: brtrue.s L_0010
    L_000a: pop 
    L_000b: newobj instance void [mscorlib]System.Object::.ctor()
    L_0010: stloc.0 
    L_0011: br.s L_0013
    L_0013: ldloc.0 
    L_0014: ret 
}

这有效地加载了_bar字段,然后检查它的存在,然后跳到最后。没有同步,因为这是多个 IL 指令,辅助线程可能会导致竞争条件 - 导致返回的对象与一组不同。

通过Lazy<T>. 这提供了一个线程安全的惰性实例化模式。当然,上面的代码没有进行延迟实例化(而是每次都返回一个新对象,直到_bar设置了某个时间),但我怀疑这是一个错误,而不是预期的行为。

此外,Lazy<T>使设置变得困难。

以线程安全的方式复制上述行为需要显式同步。


至于你的更新:

Bar 属性的 getter 永远不会返回 null。

查看上面的 IL,它(通过 ldfld)然后使用brtrue.s_bar检查该对象是否不为空。如果对象不为空,它会跳转,通过stloc.0将 的值从执行堆栈复制到本地,然后返回 - 返回一个真实值。_bar_bar

如果_bar未设置,则将其从执行堆栈中弹出,并创建一个新对象,然后将其存储并返回。

任何一种情况都会阻止null返回值。但是,我通常不会认为这种线程安全,因为在调用 get 的同时调用 set 可能会导致返回不同的对象,这是一个竞争条件,即哪个对象返回实例(设置值或新对象)。

于 2011-01-06T20:49:28.863 回答
4

我不会使用“线程安全”这个词来指代这一点。相反,我会问一个问题,其中哪一个与空合并运算符相同?

get { return _bar != null ? _bar : new Object(); }

或者

get
{
    Object result = _bar;
    if(result == null)
    {
        result = new Object();
    }
    return result;
}

从阅读其他回复来看,它似乎编译为第二个,而不是第一个。正如您所指出的,第一个可以返回 null,但第二个永远不会。

这个线程安全吗?从技术上讲,没有。阅读后_bar,不同的线程可以修改_bar,getter 将返回一个过时的值。但从你问这个问题的方式来看,我认为这就是你要找的。

编辑:这是一种避免整个问题的方法。由于value是局部变量,因此无法在后台更改。

public class Foo
{
    Object _bar = new Object();
    public Object Bar
    {
        get { return _bar; }
        set { _bar = value ?? new Object(); }
    }
}

编辑2:

这是我从 Release 编译中看到的 IL,以及我对 IL 的解释。

.method public hidebysig specialname instance object get_Bar_NullCoalesce() cil managed
{
    .maxstack 8
    L_0000: ldarg.0                         // Load argument 0 onto the stack (I don't know what argument 0 is, I don't understand this statement.)
    L_0001: ldfld object CoalesceTest::_bar // Loads the reference to _bar onto the stack.
    L_0006: dup                             // duplicate the value on the stack.
    L_0007: brtrue.s L_000f                 // Jump to L_000f if the value on the stack is non-zero. 
                                            // I believe this consumes the value on the top of the stack, leaving the original result of ldfld as the only thing on the stack.
    L_0009: pop                             // remove the result of ldfld from the stack.
    L_000a: newobj instance void [mscorlib]System.Object::.ctor()
                                            // create a new object, put a reference to it on the stack.
    L_000f: ret                             // return whatever's on the top of the stack.
}

这是我从其他方法中看到的:

.method public hidebysig specialname instance object get_Bar_IntermediateResultVar() cil managed
{
    .maxstack 1
    .locals init (
        [0] object result)
    L_0000: ldarg.0 
    L_0001: ldfld object CoalesceTest::_bar
    L_0006: stloc.0 
    L_0007: ldloc.0 
    L_0008: brtrue.s L_0010
    L_000a: newobj instance void [mscorlib]System.Object::.ctor()
    L_000f: stloc.0 
    L_0010: ldloc.0 
    L_0011: ret 
}

.method public hidebysig specialname instance object get_Bar_TrinaryOperator() cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: ldfld object CoalesceTest::_bar
    L_0006: brtrue.s L_000e
    L_0008: newobj instance void [mscorlib]System.Object::.ctor()
    L_000d: ret 
    L_000e: ldarg.0 
    L_000f: ldfld object CoalesceTest::_bar
    L_0014: ret 
}

在 IL 中,很明显,它_bar使用三元运算符读取了两次字段,但只有一次使用 null 合并和中间结果 var。此外,null coalesce 方法的 IL 非常接近于中间结果 var 方法。

这是我用来生成这些的来源:

public object Bar_NullCoalesce
{
    get { return this._bar ?? new Object(); }
}

public object Bar_IntermediateResultVar
{
    get
    {
        object result = this._bar;
        if (result == null) { result = new Object(); }
        return result;
    }
}

public object Bar_TrinaryOperator
{
    get { return this._bar != null ? this._bar : new Object(); }
}
于 2011-01-06T21:06:28.247 回答
2

getter 永远不会返回 null

这是因为当对变量( _bar) 执行读取时,会计算表达式并且结果对象(或 null)然后“释放”变量( _bar)。这是第一次评估的结果,然后“传递”给合并运算符。(参见 Reed 对 IL 的好回答。)

但是,这不是线程安全的,并且由于与上述相同的原因,分配可能很容易丢失。

于 2011-01-06T20:49:15.447 回答
0

反射器说不:

List<int> l = null;
var x = l ?? new List<int>();

编译为:

[STAThread]
public static void Main(string[] args)
{
    List<int> list = null;
    if (list == null)
    {
        new List<int>();
    }
}

在您提到的方面,这似乎不是线程安全的。

于 2011-01-06T20:54:24.113 回答