Pasar propiedades por referencia en C #


224

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 está bastante claro lo que estoy tratando de lograr. Básicamente quiero GetStringcopiar el contenido de una cadena de entrada a la WorkPhonepropiedad de Client.

¿Es posible pasar una propiedad por referencia?


En cuanto a por qué, vea este stackoverflow.com/questions/564557/…
nawfal

Respuestas:


423

Las propiedades no se pueden pasar por referencia. Aquí hay algunas maneras en que puede solucionar 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))
    {
        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
Amo los ejemplos. Me parece que este es un gran lugar para los métodos de extensión también: codecadena estática pública GetValueOrDefault (esta cadena s, cadena isNullString) {if (s == null) {s = isNullString; } devoluciones; } void Main () {person.MobilePhone.GetValueOrDefault (person.WorkPhone); }
BlackjacketMack

99
En la solución 2, el segundo parámetro getOutputes innecesario.
Jaider

31
Y creo que un mejor nombre para la solución 3 es Reflection.
Jaider

1
En la solución 2, el segundo parámetro getOutput es innecesario, es cierto, pero lo usé dentro de GetString para ver cuál era el valor que estaba configurando. No estoy seguro de cómo hacerlo sin este parámetro.
Petras

3
@GoneCodingGoodbye: pero el enfoque menos eficiente. Usar la reflexión para simplemente asignar un valor a una propiedad es como tomar un mazo para romper una nuez. Además, un método GetStringque se supone que establece una propiedad está claramente mal nombrado.
Tim Schmelter

27

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

44
+1 por cambiar el nombre GetStringa NullSafeSet, porque el primero no tiene sentido aquí.
Camilo Martin

25

Escribí un contenedor 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");

3
La mejor respuesta aquí. ¿Sabes cuál es el impacto en el rendimiento? Sería bueno tenerlo cubierto en respuesta. No estoy muy familiarizado con los árboles de expresión, pero esperaría que usar Compile () significa que la instancia de acceso contiene código compilado de IL y, por lo tanto, usar un número constante de accesores n-veces estaría bien, pero usar un total de n accesores ( alto costo de ctor) no lo haría.
mancze

Gran código! Mi opinión, es la mejor respuesta. El más genérico. Como dice mancze ... Debería tener un gran impacto en el rendimiento y debería usarse solo en un contexto donde la claridad del código es más importante que el rendimiento.
Eric Ouellet

5

Si desea obtener y establecer la propiedad de ambos, 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);
    }
}

3

Otro truco que aún no se menciona es que la clase que implementa una propiedad (por ejemplo, Foode 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án su representación interna de Fooa El procedimiento suministrado como refparámetro. Esto tiene un par de grandes ventajas sobre otros métodos de trabajo con la propiedad:

  1. La propiedad se actualiza "en el lugar"; si la propiedad es de un tipo que es compatible con los métodos 'Interbloqueados', o si es una estructura con campos expuestos de tales tipos, los métodos 'Interbloqueados' pueden usarse 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 la misma.
  3. Si el método `ActByRef` pasa uno o más parámetros` ref` desde su interlocutor al delegado suministrado, puede ser posible usar un singleton o un delegado 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 las personas que llaman no hagan nada en su devolución de llamada que pueda requerir otro bloqueo, puede ser práctico hacer que el método proteja el acceso a la propiedad con un bloqueo, de modo que las actualizaciones que no sean compatibles con `CompareExchange` aún podrían realizarse de forma cuasi-atómica.

Pasar cosas refes un patrón excelente; Lástima que no se use más.


3

Solo una pequeña expansión a la solución de expresión Linq de Nathan . Utilice el parámetro multi genérico para que la propiedad no se limite a la cadena.

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

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


2

Esto no es posible. Tu puedes decir

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

donde WorkPhonees una stringpropiedad grabable y la definición de GetStringse cambia a

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

Esto tendrá la misma semántica que 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 a los que se puede acceder mediante una sintaxis similar a un campo. Cuando intentas llamar GetStringcomo has propuesto, lo que estás pasando es un valor y no una variable. El valor que está pasando es el que devuelve el captador get_WorkPhone.


1

Lo que podría intentar hacer es crear un objeto para mantener el valor de la propiedad. De esa manera, podría pasar el objeto y aún tener acceso a la propiedad dentro.


1

¿Las propiedades no se pueden pasar por referencia? Conviértalo en un campo y use la propiedad para hacer referencia públicamente:

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

No puede refuna propiedad, pero si sus funciones necesitan ambos gety setacceso, 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

La respuesta aceptada es buena si esa función está en su código y puede modificarla. Pero a veces tiene que usar un objeto y una función de una biblioteca externa y no puede cambiar la propiedad y la definición de la función. Entonces puedes usar una variable temporal.

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

0

Para votar sobre este tema, aquí hay una sugerencia activa de cómo esto podría agregarse al idioma. No estoy diciendo que esta sea la mejor manera de hacer esto (en absoluto), siéntase libre de presentar su propia sugerencia. Pero permitir que las propiedades pasen por referencia como lo puede hacer Visual Basic ayudaría enormemente a simplificar algo de código, ¡y con bastante frecuencia!

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.