Propriedades automáticas de carregamento lento C #

100

Em C #,

Existe uma maneira de transformar uma propriedade automática em uma propriedade automática carregada lentamente com um valor padrão especificado?

Basicamente, estou tentando transformar isso ...

private string _SomeVariable

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

          return _SomeVariable;
     }
}

em algo diferente, onde posso especificar o padrão e ele trata o resto automaticamente ...

[SetUsing(SomeClass.IOnlyWantToCallYouOnce())]
public string SomeVariable {get; private set;}
ctorx
fonte
@Gabe: Observe que a classe só será chamada uma vez se nunca retornar nulo.
RedFilter
Eu descobri que ... parece que usa o padrão singleton
ctorx

Respostas:

112

Não, não há. As propriedades implementadas automaticamente funcionam apenas para implementar as propriedades mais básicas: campo de apoio com getter e setter. Não suporta este tipo de personalização.

No entanto, você pode usar o Lazy<T>tipo 4.0 para criar este padrão

private Lazy<string> _someVariable =new Lazy<string>(SomeClass.IOnlyWantToCallYouOnce);
public string SomeVariable => _someVariable.Value;

Este código calculará preguiçosamente o valor da _someVariableprimeira vez que a Valueexpressão é chamada. Será calculado apenas uma vez e armazenará o valor em cache para usos futuros da Valuepropriedade

JaredPar
fonte
1
Na verdade, me parece que o Lazy implementa o padrão singleton. Esse não é meu objetivo ... meu objetivo é criar uma propriedade carregada lentamente que é instanciada lentamente, mas disposta junto com a instância da classe em que reside. Lazy não parece estar agindo dessa forma.
ctorx
19
@ctorx Lazy não tem nada a ver com o padrão singleton. Ele faz exatamente o que você deseja.
user247702
8
Observe que SomeClass.IOnlyWantToCallYouOnceem seu exemplo deve ser estático para ser usado com um inicializador de campo.
rory.ap
Resposta incrível. Veja minha resposta para um snippet do Visual Studio que você pode usar se espera ter muitas propriedades lazy.
Zephryl
40

Provavelmente, o mais conciso que você pode obter é usar o operador de coalescência nula:

get { return _SomeVariable ?? (_SomeVariable = SomeClass.IOnlyWantToCallYouOnce()); }
Gabe Moothart
fonte
10
No caso de IOnlyWantToCallYouOncedevoluções null, irá chamá-lo mais de uma vez.
JaredPar
9
Ao usar o operador de coalescência nula, o exemplo acima falhará. A sintaxe correta é: _SomeVariable ?? ( _SomeVariable = SomeClass.IOnlyWantToCallYouOnce() );- observe a adição do parêntese ao redor da configuração _SomeVariablese for nulo.
Metro Smurf de
Essa é a melhor opção. Usei primeiro Lazy<>, mas para nossos propósitos funcionou melhor. Com o C # mais recente, ele também pode ser escrito de forma ainda mais concisa. => _SomeVariable ?? (_SomeVariable = SomeClass.IOnlyWantToCallYouOnce());O que alguns podem não notar à primeira vista é que o operador avalia o operando à direita e retorna seu resultado .
RunninglVlan
15

Há um novo recurso no C # 6 chamado Expression Bodied Auto-Properties , que permite que você escreva um pouco mais limpo:

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

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

Agora pode ser escrito como:

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

   public string SomeVariable => _someVariable.Value;
}
Alexander Derck
fonte
Na última seção do código, a inicialização não é realmente lenta. IOnlyWantToCallYouOnceseria chamado durante a construção sempre que a classe fosse instanciada.
Tom Blodget
Em outras palavras, isso não é carregado lentamente?
Zapnologica
@Zapnologica Minha resposta anterior estava um pouco errada, mas eu a atualizei. SomeVariableé carregado lentamente.
Alexander Derck
Esta resposta se parece mais com uma proposta de Expression Bodied Auto-Properties.
Little Endian,
@AbleArcher Mostrar um novo recurso de linguagem é um argumento de venda agora?
Alexander Derck
5

Não assim, os parâmetros dos atributos devem ter valores constantes, você não pode chamar o código (Mesmo código estático).

No entanto, você pode ser capaz de implementar algo com Aspectos do PostSharp.

Confira:

PostSharp

Aren
fonte
5

Aqui está minha implementação de uma solução para o seu problema. Basicamente, a ideia é uma propriedade que será definida por uma função no primeiro acesso e os acessos subsequentes produzirão o mesmo valor de retorno do primeiro.

public class LazyProperty<T>
{
    bool _initialized = false;
    T _result;

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

Então, para usar:

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

Obviamente, há a sobrecarga de passar o ponteiro de função, mas ele faz o trabalho para mim e não noto muita sobrecarga em comparação com a execução do método repetidamente.

deepee1
fonte
Não faria mais sentido dar a função ao construtor? Dessa forma, você não o criaria em linha todas as vezes e poderia descartá-lo depois de usá-lo pela primeira vez.
Mikkel R. Lund
@ lund.mikkel sim, isso também funcionaria. Podem ser casos de uso para ambas as abordagens.
deepee1
5
Se você passar a função para o construtor, da mesma forma que a classe Lazy do .Net, então a função passada terá que ser estática, eu sei que isso não se ajusta ao meu projeto em muitos casos.
crocante
@ MikkelR.Lund Às vezes você não deseja executar algum código no construtor, mas apenas sob demanda (e armazenar em cache o resultado na forma de um campo de apoio)
mamuesstack
3

Sou um grande fã dessa ideia e gostaria de oferecer o seguinte snippet C #, que chamei de proplazy.snippet. (Você pode importá-lo ou colá-lo na pasta padrão que pode obter no Gerenciador de trechos)

Aqui está um exemplo de sua saída:

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

Aqui está o conteúdo do arquivo de snippet: (salvar como 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>
Zephryl
fonte
2

Não acho que isso seja possível com C # puro. Mas você pode fazer isso usando um reescritor IL como o PostSharp . Por exemplo, permite adicionar manipuladores antes e depois das funções, dependendo dos atributos.

CodesInChaos
fonte
1

Eu fiz assim:

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

e depois você pode usá-lo como

       public virtual bool SomeProperty => this.LazyValue(() =>
    {
        return true; 
    });
Alexander Zuban
fonte
Como faço para usar "isso" neste contexto?
Riera
@Riera o que você quer dizer? Exatamente como uma propriedade normal. Por exemplo public ISet<String> RegularProperty {get;set} public string CalculatedProperty => this.LazyValue(() => { return string.Join(",", RegularProperty.ToArray()); });
Alexander Zuban
1

Operator ?? = está disponível em C # 8.0 e posterior, então agora você pode fazer de forma ainda mais concisa:

private string _someVariable;

public string SomeVariable => _someVariable ??= SomeClass.IOnlyWantToCallYouOnce();
Carlos Pozos
fonte
0

https://github.com/bcuff/AutoLazy usa Fody para dar a você algo assim

public class MyClass
{
    // This would work as a method, e.g. GetSettings(), as well.
    [Lazy]
    public static Settings Settings
    {
        get
        {
            using (var fs = File.Open("settings.xml", FileMode.Open))
            {
                var serializer = new XmlSerializer(typeof(Settings));
                return (Settings)serializer.Deserialize(fs);
            }
        }
    }

    [Lazy]
    public static Settings GetSettingsFile(string fileName)
    {
        using (var fs = File.Open(fileName, FileMode.Open))
        {
            var serializer = new XmlSerializer(typeof(Settings));
            return (Settings)serializer.Deserialize(fs);
        }
    }
}
Sam
fonte
0
[Serializable]
public class RaporImza
{
    private readonly Func<ReportConfig> _getReportLayout;
    public RaporImza(Func<ReportConfig> getReportLayout)
    {
        _getReportLayout = getReportLayout;
    }

    private ReportConfig _getReportLayoutResult;
    public ReportConfig GetReportLayoutResult => _getReportLayoutResult ?? (_getReportLayoutResult = _getReportLayout());

    public string ImzaAtanKisiAdi => GetReportLayoutResult.ReportSignatureName;

    public string ImzaAtanKisiUnvani => GetReportLayoutResult.ReportSignatureTitle;
    public byte[] Imza => GetReportLayoutResult.ReportSignature;
}

e eu chamo como abaixo

result.RaporBilgisi = new ExchangeProgramPersonAllDataModel.RaporImza(() => _reportConfigService.GetReportLayout(documentTypeId));
murat_yuceer
fonte
1
Embora possa responder à pergunta do autor, faltam algumas palavras explicativas e links para a documentação. Trechos de código bruto não são muito úteis sem algumas frases em torno dele. Você também pode descobrir como escrever uma boa resposta muito útil. Edite sua resposta.
hellow
0

Se você usar um construtor durante a inicialização lenta, as extensões a seguir também podem ser úteis

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

Uso

    private Dictionary<string, object> _cache;

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

                    /* _cache ?? (_cache = new Dictionary<string, object>()); */
Makeman
fonte
Existe uma vantagem em usar o seu ajudante LazyInitializer.EnsureInitialized()? Porque pelo que posso dizer, além da funcionalidade acima, LazyInitializeroferece tratamento de erros, bem como funcionalidade de sincronização. Código-fonte do LazyInitializer .
semaj1919