Passando propriedades por referência em C #


224

Estou tentando fazer o seguinte:

GetString(
    inputString,
    ref Client.WorkPhone)

private void GetString(string inValue, ref string outValue)
{
    if (!string.IsNullOrEmpty(inValue))
    {
        outValue = inValue;
    }
}

Isso está me dando um erro de compilação. Eu acho que é bem claro o que estou tentando alcançar. Basicamente eu quero GetStringpara copiar o conteúdo de uma cadeia de entrada para a WorkPhonepropriedade de Client.

É possível passar uma propriedade por referência?


Quanto ao porquê, consulte este stackoverflow.com/questions/564557/...
Nawfal

Respostas:


423

As propriedades não podem ser passadas por referência. Aqui estão algumas maneiras pelas quais você pode solucionar essa limitação.

1. Valor de retorno

string GetString(string input, string output)
{
    if (!string.IsNullOrEmpty(input))
    {
        return input;
    }
    return output;
}

void Main()
{
    var person = new Person();
    person.Name = GetString("test", person.Name);
    Debug.Assert(person.Name == "test");
}

2. Delegado

void GetString(string input, Action<string> setOutput)
{
    if (!string.IsNullOrEmpty(input))
    {
        setOutput(input);
    }
}

void Main()
{
    var person = new Person();
    GetString("test", value => person.Name = value);
    Debug.Assert(person.Name == "test");
}

3. Expressão LINQ

void GetString<T>(string input, T target, Expression<Func<T, string>> outExpr)
{
    if (!string.IsNullOrEmpty(input))
    {
        var expr = (MemberExpression) outExpr.Body;
        var prop = (PropertyInfo) expr.Member;
        prop.SetValue(target, input, null);
    }
}

void Main()
{
    var person = new Person();
    GetString("test", person, x => x.Name);
    Debug.Assert(person.Name == "test");
}

4. Reflexão

void GetString(string input, object target, string propertyName)
{
    if (!string.IsNullOrEmpty(input))
    {
        var prop = target.GetType().GetProperty(propertyName);
        prop.SetValue(target, input);
    }
}

void Main()
{
    var person = new Person();
    GetString("test", person, nameof(Person.Name));
    Debug.Assert(person.Name == "test");
}

2
Ame os exemplos. Acho que também é um ótimo local para métodos de extensão: codestring estática pública GetValueOrDefault (essa string s, string isNullString) {if (s == null) {s = isNullString; } retornar s; } void Main () {person.MobilePhone.GetValueOrDefault (person.WorkPhone); }
BlackjacketMack

9
Na solução 2, o segundo parâmetro getOutputé desnecessário.
Jaider

31
E acho que um nome melhor para a solução 3 é Reflexão.
213 Jaider

1
Na solução 2, o segundo parâmetro getOutput é desnecessário - verdadeiro, mas eu o usei dentro de GetString para ver qual era o valor que eu estava definindo. Não tenho certeza de como fazer isso sem esse parâmetro.
Petras 24/03

3
@GoneCodingGoodbye: mas a abordagem menos eficiente. Usar a reflexão para simplesmente atribuir um valor a uma propriedade é como usar uma marreta para quebrar uma noz. Além disso, um método GetStringque deve definir uma propriedade é claramente nomeado incorretamente.
21816 Tim Schmelter

27

sem duplicar a propriedade

void Main()
{
    var client = new Client();
    NullSafeSet("test", s => client.Name = s);
    Debug.Assert(person.Name == "test");

    NullSafeSet("", s => client.Name = s);
    Debug.Assert(person.Name == "test");

    NullSafeSet(null, s => client.Name = s);
    Debug.Assert(person.Name == "test");
}

void NullSafeSet(string value, Action<string> setter)
{
    if (!string.IsNullOrEmpty(value))
    {
        setter(value);
    }
}

4
+1 para alterar o nome GetStringpara NullSafeSet, porque o primeiro não faz sentido aqui.
Camilo Martin

25

Eu escrevi um wrapper usando a variante ExpressionTree ec # 7 (se alguém estiver interessado):

public class Accessor<T>
{
    private Action<T> Setter;
    private Func<T> Getter;

    public Accessor(Expression<Func<T>> expr)
    {
        var memberExpression = (MemberExpression)expr.Body;
        var instanceExpression = memberExpression.Expression;
        var parameter = Expression.Parameter(typeof(T));

        if (memberExpression.Member is PropertyInfo propertyInfo)
        {
            Setter = Expression.Lambda<Action<T>>(Expression.Call(instanceExpression, propertyInfo.GetSetMethod(), parameter), parameter).Compile();
            Getter = Expression.Lambda<Func<T>>(Expression.Call(instanceExpression, propertyInfo.GetGetMethod())).Compile();
        }
        else if (memberExpression.Member is FieldInfo fieldInfo)
        {
            Setter = Expression.Lambda<Action<T>>(Expression.Assign(memberExpression, parameter), parameter).Compile();
            Getter = Expression.Lambda<Func<T>>(Expression.Field(instanceExpression,fieldInfo)).Compile();
        }

    }

    public void Set(T value) => Setter(value);

    public T Get() => Getter();
}

E use-o como:

var accessor = new Accessor<string>(() => myClient.WorkPhone);
accessor.Set("12345");
Assert.Equal(accessor.Get(), "12345");

3
Melhor resposta aqui. Você sabe qual é o impacto no desempenho? Seria bom tê-lo coberto dentro de resposta. Não estou familiarizado muito com as árvores de expressão, mas espero que, usando Compile (), a instância do acessador contenha realmente o código compilado IL e, portanto, o número constante de acessadores n-times seria bom, mas usar o total de n acessadores ( alto custo de produção) não.
mancze

Ótimo código! Na minha opinião, é a melhor resposta. O mais genérico. Como diz mancze ... Ele deve ter um enorme impacto no desempenho e deve ser usado apenas em um contexto em que a clareza do código é mais importante que o desempenho.
Eric Ouellet

5

Se você deseja obter e definir a propriedade, você pode usá-lo no C # 7:

GetString(
    inputString,
    (() => client.WorkPhone, x => client.WorkPhone = x))

void GetString(string inValue, (Func<string> get, Action<string> set) outValue)
{
    if (!string.IsNullOrEmpty(outValue))
    {
        outValue.set(inValue);
    }
}

3

Outro truque ainda não mencionado é ter a classe que implementa uma propriedade (por exemplo, Foo do tipo Bar) também defina um delegado delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);e implemente um método ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1)(e possivelmente versões para dois e três "parâmetros extras") que passará sua representação interna de Foopara o procedimento fornecido como um refparâmetro. Isso tem algumas grandes vantagens sobre outros métodos de trabalho com a propriedade:

  1. A propriedade é atualizada "no local"; se a propriedade for de um tipo compatível com os métodos `Interlocked`, ou se for uma estrutura com campos expostos desses tipos, os métodos` Interlocked` poderão ser usados ​​para executar atualizações atômicas na propriedade.
  2. Se a propriedade for uma estrutura de campo exposto, os campos da estrutura poderão ser modificados sem a necessidade de fazer cópias redundantes.
  3. Se o método `ActByRef` passa um ou mais parâmetros` ref` do chamador para o delegado fornecido, pode ser possível usar um delegado singleton ou estático, evitando assim a necessidade de criar encerramentos ou delegados em tempo de execução.
  4. A propriedade sabe quando está sendo "trabalhada". Embora seja sempre necessário ter cuidado ao executar código externo enquanto mantém um bloqueio, se alguém puder confiar que os chamadores não façam nada em seu retorno de chamada que exija outro bloqueio, pode ser prático que o método proteja o acesso à propriedade com um bloqueio, de forma que as atualizações que não são compatíveis com o `CompareExchange` ainda possam ser executadas quase-atomicamente.

Passar as coisas refé um excelente padrão; Pena que não é mais usado.


3

Apenas um pouco de expansão para a solução Linq Expression de Nathan . Use parâmetros multi-genéricos para que a propriedade não se limite à string.

void GetString<TClass, TProperty>(string input, TClass outObj, Expression<Func<TClass, TProperty>> outExpr)
{
    if (!string.IsNullOrEmpty(input))
    {
        var expr = (MemberExpression) outExpr.Body;
        var prop = (PropertyInfo) expr.Member;
        if (!prop.GetValue(outObj).Equals(input))
        {
            prop.SetValue(outObj, input, null);
        }
    }
}

2

Isso é abordado na seção 7.4.1 da especificação do idioma C #. Somente uma referência variável pode ser passada como um parâmetro ref ou out em uma lista de argumentos. Uma propriedade não se qualifica como uma referência variável e, portanto, não pode ser usada.


2

Isso não é possível. Você poderia dizer

Client.WorkPhone = GetString(inputString, Client.WorkPhone);

onde WorkPhoneé uma stringpropriedade gravável e a definição de GetStringé alterada para

private string GetString(string input, string current) { 
    if (!string.IsNullOrEmpty(input)) {
        return input;
    }
    return current;
}

Isso terá a mesma semântica que você parece estar tentando.

Isso não é possível porque uma propriedade é realmente um par de métodos disfarçados. Cada propriedade disponibiliza getters e setters que são acessíveis por meio de sintaxe do tipo campo. Quando você tenta ligar GetStringcomo propôs, o que está passando é um valor e não uma variável. O valor que você está passando é o retorno do getter get_WorkPhone.


1

O que você poderia tentar fazer é criar um objeto para armazenar o valor da propriedade. Dessa forma, você pode passar o objeto e ainda ter acesso à propriedade dentro.


1

Propriedades não podem ser passadas por referência? Torne-o um campo e use a propriedade para referenciá-lo publicamente:

public class MyClass
{
    public class MyStuff
    {
        string foo { get; set; }
    }

    private ObservableCollection<MyStuff> _collection;

    public ObservableCollection<MyStuff> Items { get { return _collection; } }

    public MyClass()
    {
        _collection = new ObservableCollection<MyStuff>();
        this.LoadMyCollectionByRef<MyStuff>(ref _collection);
    }

    public void LoadMyCollectionByRef<T>(ref ObservableCollection<T> objects_collection)
    {
        // Load refered collection
    }
}

0

Você não pode refuma propriedade, mas se suas funções precisarem de ambos gete de setacesso, você pode passar por uma instância de uma classe com uma propriedade definida:

public class Property<T>
{
    public delegate T Get();
    public delegate void Set(T value);
    private Get get;
    private Set set;
    public T Value {
        get {
            return get();
        }
        set {
            set(value);
        }
    }
    public Property(Get get, Set set) {
        this.get = get;
        this.set = set;
    }
}

Exemplo:

class Client
{
    private string workPhone; // this could still be a public property if desired
    public readonly Property<string> WorkPhone; // this could be created outside Client if using a regular public property
    public int AreaCode { get; set; }
    public Client() {
        WorkPhone = new Property<string>(
            delegate () { return workPhone; },
            delegate (string value) { workPhone = value; });
    }
}
class Usage
{
    public void PrependAreaCode(Property<string> phone, int areaCode) {
        phone.Value = areaCode.ToString() + "-" + phone.Value;
    }
    public void PrepareClientInfo(Client client) {
        PrependAreaCode(client.WorkPhone, client.AreaCode);
    }
}

0

A resposta aceita é boa se essa função estiver no seu código e você puder modificá-la. Mas, às vezes, você precisa usar um objeto e uma função de alguma biblioteca externa e não pode alterar a definição de propriedade e função. Então você pode apenas usar uma variável temporária.

var phone = Client.WorkPhone;
GetString(input, ref phone);
Client.WorkPhone = phone;

0

Para votar nesta questão, aqui está uma sugestão ativa de como isso pode ser adicionado ao idioma. Não estou dizendo que essa é a melhor maneira de fazer isso (sinta-se), fique à vontade para colocar sua própria sugestão. Mas permitir que as propriedades sejam passadas por ref como o Visual Basic já pode fazer ajudaria enormemente a simplificar algum código, e com bastante frequência!

https://github.com/dotnet/csharplang/issues/1235

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.