Передача властивостей за посиланням у C #


224

Я намагаюся зробити наступне:

GetString(
    inputString,
    ref Client.WorkPhone)

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

Це дає мені помилку компіляції. Я думаю, що досить зрозуміло, чого я намагаюся досягти. В основному я хочу GetStringскопіювати вміст вхідного рядка у WorkPhoneвластивість Client.

Чи можливо передати об'єкт нерухомості за посиланням?


Щодо того, дивіться цей stackoverflow.com/questions/564557/…
Навфал

Відповіді:


423

Властивості не можна передавати за посиланням. Ось кілька способів подолати це обмеження.

1. Повернене значення

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. Делегат

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. Вираз 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. Рефлексія

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
Любіть приклади. Я вважаю, що це чудове місце і для методів розширення: codepublic static string GetValueOrDefault (цей рядок s, string isNullString) {if (s == null) {s = isNullString; } повернути s; } void Main () {person.MobilePhone.GetValueOrDefault (person.WorkPhone); }
BlackjacketMack

9
У рішенні 2 2-й параметр getOutputнепотрібний.
Джайдер

31
І я думаю, що краща назва рішення 3 - «Роздум».
Jaider

1
У рішенні 2 2-й параметр getOutput є непотрібним - правда, але я використовував його всередині GetString, щоб побачити, яке значення я встановив. Не знаєте, як це зробити без цього параметра.
Петрас

3
@GoneCodingGoodbye: але найменш ефективний підхід. Використовувати відображення для простого присвоєння значення властивості - це як взяти кувалду, щоб зламати гайку. Також метод, GetStringякий повинен встановлювати властивість, явно неправильно називається.
Тім Шмелтер

27

не дублюючи майно

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 для зміни імені GetStringна NullSafeSet, тому що колишнє тут не має сенсу.
Каміло Мартін

25

Я написав обгортку, використовуючи варіант ExpressionTree і c # 7 (якщо когось цікавить):

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

І використовувати його так:

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

3
Найкраща відповідь тут. Чи знаєте ви, що таке вплив на продуктивність? Було б добре, щоб це було висвітлено у відповіді. Я мало знайомий з деревами виразів, але я би сподівався, що використання компіляції () означає, що екземпляр аксесуара містить фактично скомпільований код IL, і тому використання постійної кількості аксесуарів n-разів було б нормально, але з використанням загальної кількості n ( висока вартість ctor) не буде.
mancze

Чудовий код! На мою думку, це найкраща відповідь. Найбільш загальний. Як каже Mancze ... Це повинно мати величезний вплив на продуктивність, і його слід використовувати лише в контексті, коли чіткість коду важливіша за ефективність.
Ерік Оуеллет

5

Якщо ви хочете отримати та встановити властивість як обидва, ви можете використовувати це в 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

Ще одна хитрість ще не згадується, щоб мати клас , який реалізує властивість (наприклад , Fooтипу Bar) також визначає делегат delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);і реалізувати метод ActOnFoo<TX1>(ref Bar it, ActByRef<Bar,TX1> proc, ref TX1 extraParam1)(і , можливо , версії для двох і трьох «додаткових параметрів», а) , який буде проходити його внутрішнє подання Fooв надана процедура як refпараметр. Це має кілька великих переваг перед іншими методами роботи з майном:

  1. Властивість оновлюється "на місці"; якщо властивість має тип, сумісний із методами `Interlocked`, або якщо це структура із відкритими полями таких типів, для активації атомних оновлень властивості може використовуватися метод` Interlocked`.
  2. Якщо властивість є структурою відкритого поля, поля структури можуть бути змінені без необхідності робити надмірні її копії.
  3. Якщо метод `ActByRef` передає один або більше параметрів` ref` через свого виклику до наданого делегата, можливо, можливо використовувати однотонний або статичний делегат, тим самим уникнути необхідності створювати закриття або делегати під час виконання.
  4. Власність знає, коли над ним "працює". Хоча завжди потрібно обережно виконувати зовнішній код під час утримування блокування, якщо можна довіряти абонентам не робити занадто нічого у своєму зворотному дзвінку, що може зажадати іншого блокування, може бути практичним, щоб метод захищав доступ до властивості за допомогою Блокування, таким чином, що оновлення, несумісні з функцією "CompareExchange", все ще можуть проводитися квазіатомно.

Передати речі ref- це відмінний зразок; занадто погано, що більше не використовується.


3

Лише невелике розширення до рішення Натана Linq Expression . Використовуйте мульти-загальний парам, щоб властивість не обмежувалася рядком.

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

Це висвітлено у розділі 7.4.1 специфікації мови C #. Лише змінна посилання може бути передана як параметр ref або out у списку аргументів. Властивість не кваліфікується як посилання на змінну, і тому її не можна використовувати.


2

Це неможливо. Можна сказати

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

де властивість для WorkPhoneзапису stringі визначення GetStringзмінено на

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

Це матиме ту саму семантику, яку ви, здається, намагаєтесь.

Це неможливо, оскільки властивість - це насправді пара методів у маскуванні. Кожна власність робить доступними геттери та сетери, які доступні через синтаксис, подібний до поля. Коли ви намагаєтесь зателефонувати, GetStringяк ви запропонували, те, що ви передаєте, - це значення, а не змінна. Значення, яке ви передаєте, - це те, що повернулося від геттера get_WorkPhone.


1

Що ви можете спробувати зробити, це створити об’єкт для зберігання значення властивості. Таким чином ви могли передавати об’єкт і все ще мати доступ до власності всередині.


1

Неможливо передати властивості за посиланням? Зробіть це поле і використовуйте властивість для посилання на нього:

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

Ви не можете refмати властивість, але якщо вашим функціям потрібні обидва getта setдоступ, ви можете пройти навколо екземпляра класу із визначеним властивістю:

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

Приклад:

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

Прийнята відповідь хороша, якщо ця функція є у вашому коді, і ви можете її змінити. Але іноді доводиться використовувати об'єкт і функцію з якоїсь зовнішньої бібліотеки, і ви не можете змінити властивість і визначення функції. Тоді ви можете просто використовувати тимчасову змінну.

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

0

Щоб проголосувати за це питання, ось одна активна пропозиція, як це можна додати до мови. Я не кажу, що це найкращий спосіб зробити це (зовсім), не соромтеся висловити власну пропозицію. Але дозволити передавати властивості за допомогою ref, як уже це може зробити Visual Basic, дуже допоможе спростити деякий код, причому досить часто!

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.