Pasar propiedades por referencia en C#


Estoy tratando de hacer lo siguiente:

GetString(
    inputString,
    ref Client.WorkPhone)

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

Esto me está dando un error de compilación. Creo que es bastante claro lo que estoy tratando de lograr. Básicamente quiero GetString copiar el contenido de una cadena de entrada a la propiedad WorkPhone de Client.

¿ Es posible pasar una propiedad por referencia?

Author: AFract, 2009-09-10

10 answers

Las propiedades no se pueden pasar por referencia. Aquí hay algunas maneras en que puede evitar esta limitación.

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. Expresión 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. Reflexión

void GetString(string input, object target, string propertyName)
{
    if (!string.IsNullOrEmpty(input))
    {
        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");
}
 333
Author: Nathan Baulch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-28 16:04:13

Sin duplicar la propiedad

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);
    }
}
 22
Author: Firo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-24 09:47:30

Escribí un wrapper usando la variante ExpressionTree y c # 7 (si alguien está interesado):

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

Y úsalo como:

var accessor = new Accessor<string>(() => myClient.WorkPhone);
accessor.Set("12345");
Assert.Equal(accessor.Get(), "12345");
 14
Author: Sven,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-19 14:40:58

Otro truco aún no mencionado es hacer que la clase que implementa una propiedad (por ejemplo, Foo de tipo Bar) también defina un delegado delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2); e implemente un método ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1) (y posiblemente versiones para dos y tres "parámetros adicionales" también) que pasará su representación interna de Foo al procedimiento suministrado como un parámetro ref. Esto tiene un par de grandes ventajas sobre otros métodos de trabajo con la propiedad:

  1. La propiedad se actualiza "en su lugar"; si la propiedad es de un tipo que es compatible con métodos` Entrelazados`, o si es una estructura con campos expuestos de tales tipos, los métodos` Entrelazados ' se pueden usar para realizar actualizaciones atómicas a la propiedad.
  2. Si la propiedad es una estructura de campo expuesto, los campos de la estructura pueden modificarse sin tener que hacer copias redundantes de ella.
  3. Si el método` ActByRef 'pasa uno o más parámetros' ref ' a través de su llamador al delegado suministrado, puede ser posible utilice un delegado singleton o estático, evitando así la necesidad de crear cierres o delegados en tiempo de ejecución.
  4. La propiedad sabe cuándo se está "trabajando con". Si bien siempre es necesario tener precaución al ejecutar código externo mientras se mantiene un bloqueo, si se puede confiar en que los llamantes no hagan demasiado nada en su devolución de llamada que pueda requerir otro bloqueo, puede ser práctico tener el método que proteja el acceso a la propiedad con un bloqueo, de modo que las actualizaciones que no son compatibles con "CompareExchange" todavía podría realizarse cuasi-atómicamente.

Passing things be ref es un excelente patrón; lástima que no se use más.

 3
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-12-16 21:37:13

Esto está cubierto en la sección 7.4.1 de la especificación del lenguaje C#. Solo una variable-reference se puede pasar como parámetro ref o out en una lista de argumentos. Una propiedad no califica como una referencia de variable y por lo tanto no se puede utilizar.

 2
Author: JaredPar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-09-10 00:27:23

Esto no es posible. Se podría decir

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

Donde WorkPhone es una propiedad de escritura string y la definición de GetString se cambia a

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

Esto tendrá la misma semántica que usted parece estar intentando.

Esto no es posible porque una propiedad es realmente un par de métodos disfrazados. Cada propiedad pone a disposición getters y setters que son accesibles a través de una sintaxis similar a un campo. Cuando intenta llamar GetString como lo ha propuesto, lo que está pasando es un valor y no una variable. El valor que está pasando es el que devuelve el getter get_WorkPhone.

 2
Author: jason,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-18 08:49:46

Solo una pequeña expansión a La solución de expresión Linq de Nathan. Use multi generic param para que la propiedad no se limite a 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
Author: Zick Zhang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-18 08:50:15

Lo que podría intentar hacer es crear un objeto que contenga el valor de la propiedad. De esa manera usted podría pasar el objeto y todavía tener acceso a la propiedad en el interior.

 1
Author: Anthony Reese,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-06-02 15:58:14

Si desea obtener y establecer la propiedad both, puede usar esto en 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);
    }
}
 1
Author: Pellet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-22 03:16:05

No puede ref una propiedad, pero si sus funciones necesitan acceso tanto get como set puede pasar una instancia de una clase con una propiedad 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;
    }
}

Ejemplo:

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
Author: chess123mate,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-08 20:11:54