Избегайте рекурсии с помощью универсального метода для установки свойств класса С#

Я следую, чтобы объяснить свою проблему в гораздо большем классе с меньшим простым для понимания точным примером. Я получил довольно большой класс с множеством свойств разных типов, получая и устанавливая соответствующие переменные класса.

public class Foo() {
    int property1 { get => _property1 ; set => _property1 = value;}
    string property2 { get => _property2 ; set => _property2 = value;}
    Vector3 property3 { get => _property3 ; set => _property3 = value;}
    bool property4 { get => _property3 ; set => _property4 = value;}
}

Я поставил в пример 4 свойства, но в реальном примере их очень много. Мне нужно применить логику в наборе всех свойств, в зависимости от логического свойства property4, поэтому вместо того, чтобы писать один и тот же код во всех установщиках моих свойств, я попытался сделать общий метод, который будет вызываться во всех из них.

Итак, я сделал перечисление:

public enum properties {
    property1,
    property2,
    property3,
    property4
}

Чтобы я мог установить свои свойства с помощью метода, который включает отражение, принимая тип свойства в качестве аргумента:

public void setLogic<T>(properties property, T value) {
    //irrelevant code
}

Итак, мои сеттеры становятся:

public class Foo() {
    int property1 { get => _property1 ; set { setLogic(properties.property1 , value) };}
    string property2 { get => _property2 ; set { setLogic(properties.property2 , value) };}
    Vector3 property3 { get => _property3 ; set { setLogic(properties.property3 , value) };}
    bool property4 { get => _property4 ; set{ _property4 = value) };}
}

Моя проблема возникает, когда в моем setLogic() установщик свойств вызывается рекурсивно, создавая переполнение стека. Поэтому я решил эту тему с помощью логического значения, управляемого из setLogic(), которое контролирует, откуда вызывается сеттер. Итак, теперь мои свойства выглядят так:

public class Foo() {
    int property1 { 
        get => _property1; 
        set { 
            if (!_calledFromSetLogic)
                setLogic(properties.property1 , value);
            else {
                _property1 = value;
                _calledFromSetLogic = false;
            }
        }
    }
    string property2 { 
        get => _property2; 
        set { 
            if (!_calledFromSetLogic)
                setLogic(properties.property2 , value);
            else {
                _property2 = value;
                _calledFromSetLogic = false;
            }
        }
    }
    Vector3 property3 { 
        get => _property3; 
        set { 
            if (!_calledFromSetLogic)
                setLogic(properties.property3 , value);
            else {
                _property3 = value;
                _calledFromSetLogic = false;
            }
        }
    }
    bool property4 { get => property4; set{ _property4 = value) };}
}

Код работает нормально, но элемент управления setter bool, чтобы избежать рекурсии, отбрасывает все возможные чистоты, созданные универсальным методом SetLogic(). С другой стороны, я не могу установить переменные класса в методе setLogic, потому что я обращаюсь к свойствам с отражением, поэтому, чтобы установить новое значение в логике, я не могу избежать рекурсивного набора без логического значения (property.SetValue() из класса отражения устанавливает новое значение, вызывая набор еще раз, поэтому бесконечный цикл).

Если я этого не сделаю, мне придется вставить метод setLogic() вместо общего, скопированного для каждого из свойств в наборе, что также не является очень чистым кодом.

Нет ли для этого чистого решения, где сеттер может быть передан в качестве аргумента, или общего метода, который позволяет избежать бесконечного рекурсивного набора?

Я думал о чем-то вроде

private setLogic<T>(Action<> setterMethod, T value) {
    //so that the property involved might be already in the setter?
}

или другой тип setLogic с общими свойствами класса, который позволяет избежать бесконечного цикла, о котором не может думать.

Надеюсь, я дал понять.


person rustyBucketBay    schedule 05.08.2020    source источник
comment
Отражение - это очень медленный способ установить значение свойства по сравнению с его прямой установкой, и вы не дали понять, с какой целью вы устанавливаете свойства таким образом; как есть, первый фрагмент кода, безусловно, самый чистый, и вы в любом случае дублируете код, вызывая setLogic во всех сеттерах, просто дублируя его по-другому. В любом случае вы должны включить функцию setLogic, потому что в ее нынешнем виде этому вопросу не хватает ясности и не хватает ключевого компонента для определения того, почему это вызывает рекурсию.   -  person Zaelin Goodman    schedule 05.08.2020
comment
Вопрос длинный. Я попытался прояснить, что делает здесь рекурсию: (property.SetValue() из класса отражения устанавливает новое значение, вызывая набор еще раз, поэтому бесконечный цикл). Если есть какие-либо сомнения по этому поводу или я могу дать какие-либо дополнительные объяснения, я буду рад.   -  person rustyBucketBay    schedule 05.08.2020
comment
Что касается медленного отражения, суть этого вопроса заключается не в эффективности, а в написании чистого/компактного кода. В сценарии, который я объяснил, я должен проверять сеттер с логическим значением, поэтому сеттеры становятся длинными, если их много. Без универсального метода я должен написать один и тот же код логики набора во всех сеттерах. В обоих случаях код становится длинным и повторяется с одинаковыми шаблонами, поэтому вопрос в том, может ли быть какой-либо другой, более компактный способ для кода этого сценария.   -  person rustyBucketBay    schedule 05.08.2020
comment
Моя рекомендация заключалась бы в том, чтобы поддержать свойство с помощью частного поля и установить это поле в вашей функции setLogic вместо вызова средства установки свойства. В качестве альтернативы перед вызовом установщика проверьте, совпадает ли новое значение с существующим значением. В качестве альтернативы, и я рекомендую это в основном потому, что единственная причина написания кода таким образом — чистота, вернитесь к использованию первого фрагмента — это самый чистый из всех доступных вариантов с точки зрения стандартизации.   -  person Zaelin Goodman    schedule 05.08.2020
comment
+1 за проверку того, совпадает ли новое значение в установщике. Что касается возврата к использованию первого фрагмента, я объясню, почему необходимая логика в установщике и невозможность доступа к закрытым переменным, поскольку я обращаюсь к свойствам с отражением, делает невозможным использование первого простого фрагмента, это было бы желательно   -  person rustyBucketBay    schedule 06.08.2020
comment
Извините, я перечитал вопрос сейчас и понимаю, почему это не было полезным предложением - мозг после работы неправильно истолковал меня. Я рад, что Джефф Э. смог лучше понять причину вопроса.   -  person Zaelin Goodman    schedule 06.08.2020


Ответы (2)


Вы можете просто использовать параметр ref для прямой установки поля?:

int property1
{
    get => _property1;
    set => setLogic(ref _property1, value);
}

private void setLogic<T>(ref T field, T value)
{
    field = value;
}

Я обычно использую этот шаблон при реализации INotifyPropertyChanged:

private int _someProperty;
public int SomeProperty
{
    get => _someProperty;
    set => SetProperty(ref _someProperty, value);
}

private void SetProperty<T>(ref T field, T value, [CallerMemberName] propertyName = "")
{
    if (!field.Equals(value))
    {
        field = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
person Jeff    schedule 05.08.2020
comment
бля я именно это и писал. Я думаю, что это хороший способ. Как осветитель MVVM. - person Orkad; 05.08.2020
comment
Этот ответ решает именно ту проблему, которую я выявил. Большое спасибо за точный ответ!! Однако то, что я устанавливаю, не является частной переменной, я получаю и устанавливаю другое свойство из вложенного класса, поэтому я получаю ошибку компиляции, когда пытаюсь передать это по ссылке: свойство или индексатор не могут быть переданы как параметр out или ref . С этого момента я постараюсь найти лучшее решение. В любом случае, я рад узнать об этом шаблоне для более чистого кода :) - person rustyBucketBay; 06.08.2020

Вы можете использовать атрибут [CallerMemberName] и Dictionary<string, object> для хранения свойств. Это не приведет к таким же потерям производительности, как отражение.

Например...

class Foo : PropertyChangeNotifier
{
    public int property1 { get { return Get<int>(); } set { Set(value); } }
    public string property2 { get { return Get<string>(); } set { Set(value); } }
    public Vector3 property3 { get { return Get<Vector3>(); } set { Set(value); } }
    public bool property4 { get { return Get<bool>(); } set { Set(value); } }

    protected override void OnSet<T>(string property, T value)
    {
        // do something meaningful.
    }
}

...и базовый класс...

abstract class PropertyChangeNotifier
{
    private readonly Dictionary<string, object> properties = new Dictionary<string, object>();

    protected T Get<T>([CallerMemberName] string property = null)
    {
        return (T)properties[property];
    }

    protected void Set<T>(T value, [CallerMemberName] string property = null)
    {
        OnSet(property, value);
        properties[property] = value;
    }

    protected abstract void OnSet<T>(string property, T value);
}
person Creyke    schedule 05.08.2020
comment
Спасибо за ответ, +1 за предложение и за // сделать что-то значимое - person rustyBucketBay; 06.08.2020