C# 惰性加载自动属性

125

在 C# 中,是否有一种方法可以将自动属性转换为具有指定默认值的延迟加载自动属性?

本质上,我正在尝试将这个...

private string _SomeVariable

public string SomeVariable
{
     get
     {
          if(_SomeVariable == null)
          {
             _SomeVariable = SomeClass.IOnlyWantToCallYouOnce();
          }

          return _SomeVariable;
     }
}

将其转换为不同的形式,我可以在其中指定默认值,并使其自动处理其余内容...

[SetUsing(SomeClass.IOnlyWantToCallYouOnce())]
public string SomeVariable {get; private set;}

@Gabe:请注意,如果它从不返回null,那么该类只会被调用一次。 - D'Arcy Rittich
我发现...它似乎使用了单例模式。 - ctorx
14个回答

130
没有这样的功能。自动实现属性仅用于实现最基本的属性:带有getter和setter的后备字段。它不支持此类自定义。
但是,您可以使用4.0版的Lazy<T>类型来创建此模式。
private Lazy<string> _someVariable =new Lazy<string>(SomeClass.IOnlyWantToCallYouOnce);
public string SomeVariable => _someVariable.Value;

这段代码将会在第一次调用Value表达式时懒惰地计算_someVariable的值。它只会被计算一次,并且会缓存该值以供未来使用Value属性时调用。


1
实际上,我觉得Lazy实现了单例模式。这不是我的目标...我的目标是创建一个懒加载的属性,它会在需要时才被实例化,但会随着其所在类的实例一起被释放。Lazy似乎没有以这种方式执行。 - ctorx
21
@ctorx:Lazy与单例模式无关,它恰好达到你想要的效果。 - user247702
13
注意,在你的例子中,SomeClass.IOnlyWantToCallYouOnce 必须是静态的才能与字段初始化器一起使用。 - rory.ap
非常棒的答案。如果您期望有许多惰性属性,可以查看我的答案中的Visual Studio代码片段。 - Zephryl

46

可能最简洁的方法是使用 null 合并运算符:

get { return _SomeVariable ?? (_SomeVariable = SomeClass.IOnlyWantToCallYouOnce()); }

13
如果IOnlyWantToCallYouOnce返回null,它将被调用多次。 - JaredPar
9
当使用空值合并运算符时,上述示例会失败。正确的语法是:_SomeVariable ?? (_SomeVariable = SomeClass.IOnlyWantToCallYouOnce()); - 注意如果_SomeVariable为空,则在设置它周围加上括号。 - Metro Smurf
这是最佳选项。起初我使用了 Lazy<>,但对于我们的目的来说,这种方式效果更好。在最新的 C# 中,它甚至可以更加简洁地编写为 => _SomeVariable ?? (_SomeVariable = SomeClass.IOnlyWantToCallYouOnce()); 有些人可能没有注意到的是,运算符会_计算右操作数并返回其结果_。 - RunninglVlan
1
C# 8 允许您执行 public object MyProp => _myProp ??= new object(); - gregsdennis

26

C# 8.0及以上版本提供了运算符??=,因此您现在可以更加简洁地进行操作:

private string _someVariable;

public string SomeVariable => _someVariable ??= SomeClass.IOnlyWantToCallYouOnce();

只需注意它不像原版那样是线程安全的。Lazy提供了内置的安全性,如果需要的话会带来一些开销。

1
这是目前针对C#8及以上版本的最佳答案。 - Matt Jenkins
7
好的。请注意,它不像原始版本那样是线程安全的。如果需要,使用Lazy<T>可以提供内置的安全性,但会带来一些开销。 - joe

21

C#6引入了一项名为表达式体自动属性的新特性,它使你可以更加简洁地编写代码:

public class SomeClass
{ 
   private Lazy<string> _someVariable = new Lazy<string>(SomeClass.IOnlyWantToCallYouOnce);

   public string SomeVariable 
   {
      get { return _someVariable.Value; }
   }
}

现在可以写成:

public class SomeClass
{
   private Lazy<string> _someVariable = new Lazy<string>(SomeClass.IOnlyWantToCallYouOnce);

   public string SomeVariable => _someVariable.Value;
}

在代码的最后一部分,初始化实际上并不是惰性的。每当实例化类时,都会调用"IOnlyWantToCallYouOnce"构造函数。 - Tom Blodget
那么换句话说,这不是惰性加载? - Zapnologica
@Zapnologica 我之前的回答有点错误,但我已经更新了它。SomeVariable是延迟加载的。 - Alexander Derck
这个回答更像是对表达式主体自动属性的推销。 - Little Endian
@AbleArcher 指出一项新的语言特性现在成了推销吗? - Alexander Derck
嗯,我喜欢JaredPar简洁的回答“没有”,然后是替代方案。抱歉。 - Little Endian

7
这是我对你的问题提供解决方案的实现。基本上,这个想法是一个属性,首次访问时将由函数设置,并且后续访问将产生与第一次相同的返回值。
public class LazyProperty<T>
{
    bool _initialized = false;
    T _result;

    public T Value(Func<T> fn)
    {
        if (!_initialized)
        {
            _result = fn();
            _initialized = true;
        }
        return _result;
    }
 }

然后使用:

LazyProperty<Color> _eyeColor = new LazyProperty<Color>();
public Color EyeColor
{ 
    get 
    {
        return _eyeColor.Value(() => SomeCPUHungryMethod());
    } 
}

当然,传递函数指针也会增加开销,但对于我来说它完成了工作,并且与一遍遍地运行该方法相比,我并没有注意到太多的性能开销。


把函数放在构造函数里面不是更合理吗?这样你就不需要每次都内联创建它,而且你可以在第一次使用后将其处理掉。 - Mikkel R. Lund
@lund.mikkel 是的,那也可以。两种方法都有可能有使用情况。 - deepee1
5
如果你将函数传递给构造函数,就像 .Net 的 Lazy 类一样,那么传入的函数必须是静态的,我知道在很多情况下这并不符合我的设计。 - crunchy
@MikkelR.Lund 有时候你不想在构造函数中执行某些代码,而是只在需要时执行(并将结果缓存到支持字段中) - mamuesstack

6

不是这样,属性的参数必须是常量值,不能调用代码(即使是静态代码)。

然而,您可以使用PostSharp的Aspect实现某些功能。

请查看以下链接:

PostSharp


4
我是这样做的:
public static class LazyCachableGetter
{
    private static ConditionalWeakTable<object, IDictionary<string, object>> Instances = new ConditionalWeakTable<object, IDictionary<string, object>>();
    public static R LazyValue<T, R>(this T obj, Func<R> factory, [CallerMemberName] string prop = "")
    {
        R result = default(R);
        if (!ReferenceEquals(obj, null))
        {
            if (!Instances.TryGetValue(obj, out var cache))
            {
                cache = new ConcurrentDictionary<string, object>();
                Instances.Add(obj, cache);

            }


            if (!cache.TryGetValue(prop, out var cached))
            {
                cache[prop] = (result = factory());
            }
            else
            {
                result = (R)cached;
            }

        }
        return result;
    }
}

然后您可以像下面这样使用它

       public virtual bool SomeProperty => this.LazyValue(() =>
    {
        return true; 
    });

我该如何在这种情况下使用“this”? - Riera
@Riera 你是什么意思?就像普通属性一样。例如:public ISet RegularProperty {get;set;} public string CalculatedProperty => this.LazyValue(() => { return string.Join(",", RegularProperty.ToArray()); }); - Alexander Zuban

3

我非常支持这个想法,并且想提供以下的 C# 代码片段,我将其称为 proplazy.snippet。(你可以导入它或将其粘贴到你可以从“代码片段管理器”中获得的标准文件夹中)

下面是它输出的一个示例:

private Lazy<int> myProperty = new Lazy<int>(()=>1);
public int MyProperty { get { return myProperty.Value; } }

以下是代码片段文件的内容:(保存为proplazy.snippet)
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>proplazy</Title>
            <Shortcut>proplazy</Shortcut>
            <Description>Code snippet for property and backing field</Description>
            <Author>Microsoft Corporation</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>type</ID>
                    <ToolTip>Property type</ToolTip>
                    <Default>int</Default>
                </Literal>
                <Literal>
                    <ID>field</ID>
                    <ToolTip>The variable backing this property</ToolTip>
                    <Default>myVar</Default>
                </Literal>
                <Literal>
                    <ID>func</ID>
                    <ToolTip>The function providing the lazy value</ToolTip>
                </Literal>
                <Literal>
                    <ID>property</ID>
                    <ToolTip>Property name</ToolTip>
                    <Default>MyProperty</Default>
                </Literal>

            </Declarations>
            <Code Language="csharp"><![CDATA[private Lazy<$type$> $field$ = new Lazy<$type$>($func$);
            public $type$ $property$ { get{ return $field$.Value; } }
            $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

我会将 myProperty 设置为 readonly,以确保安全。 - PLopes

2

我认为使用纯C#是不可能实现这个的。但你可以使用像PostSharp这样的IL重写器来完成它。例如,它允许您根据属性在函数前后添加处理程序。


0
如果您在惰性初始化期间使用构造函数,则以下扩展可能也会有所帮助。
public static partial class New
{
    public static T Lazy<T>(ref T o) where T : class, new() => o ?? (o = new T());
    public static T Lazy<T>(ref T o, params object[] args) where T : class, new() =>
            o ?? (o = (T) Activator.CreateInstance(typeof(T), args));
}

使用方法

    private Dictionary<string, object> _cache;

    public Dictionary<string, object> Cache => New.Lazy(ref _cache);

                    /* _cache ?? (_cache = new Dictionary<string, object>()); */

1
使用您的助手与LazyInitializer.EnsureInitialized()相比是否有优势?因为据我所知,除了上面的功能之外,LazyInitializer还提供错误处理和同步功能。LazyInitializer源代码 - semaj1919

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接