четверг, 5 сентября 2013 г.

"Медленные" свойства

Свойства, как правило, достаточно просты, но предлагаю сразу не переставать читать, а посмотреть на то, что будет подкатом.
А там будет общая информация о свойствах , но, самое главное будет о том, что делать, если свойство "медленное", т.е. на присвоение или чтение надо очень много времени.

Свойства
Итак, если вспомнить классику ООП, то класс объединяет данные и методы для их обработки.
Т.е. в самом простом случае, у нас может быть вот такая ситуация:
class Person
{
    private string _lastName;

    public string GetLastName()
    {
        return _lastName;
    }

    public void SetLastName(string p_lastName)
    {
        _lastName = p_lastName;
    }
}

Как видим все просто и эквивалентно вот такому варианту:
class Person
{
    public string LastName;
}

Давайте посмотрим на варианты использования первого подхода:
List<Person> persons = new List<Person>();
Person person = new Person();
person.SetLastName("Иванов");
// ... некий код
int homonymCount = persons.Count(p => p.GetLastName() == person.GetLastName());

И второго:
List<Person> persons = new List<Person>();
Person person = new Person();
person.LastName = "Иванов";
// ... некий код
int homonymCount = persons.Count(p => p.LastName == person.LastName);

Как вам не знаю, а мне больше нравится второй способ записи. Зачем тогда городить огород?
А вот зачем, мы можем сделать вот так в первом случае:
class Person
{
    private string _lastName;
 
    public string GetLastName()
    {
        return _lastName;
    }
 
    public void SetLastName(string p_lastName)
    {
        if (!string.IsNullOrWhiteSpace(p_lastName))
        {
            _lastName = p_lastName;
        }
        else
        {
            throw new ArgumentException("Фамилия должна быть не null и содержать не только пробелы");
        }
    }
}

И, вполне понятно, что второй случай такого не обеспечивает.
Поэтому в современных языках программирования появился синтаксический сахар, который и получил название свойства.
Т.е. для использования по второму варианту, а реализации функционала по первому, можно использовать синтаксис:
class Person
{
    private string _lastName;
 
    public string LastName
    {
        get
        {
            return _lastName;
        }
        set
        {
            if (!string.IsNullOrWhiteSpace(value))
            {
                _lastName = value;
            }
            else
            {
                throw new ArgumentException("Фамилия должна быть не null и содержать не только пробелы");
            }
        }
    }
}

Т.е. мы пишем и используем как нам удобно, а при компиляции все это разворачивается в те же методы чтения (аксессор) и изменения (мутатор). Именно это и было доступно во Framework 1.0. Со второго фреймворка стал доступен еще один вид синтаксического сахара. Т.к. зачастую свойство не несло дополнительной логики, то для сокращения записи применялся синтаксис:
class Person
{
    public string LastName { get; set; }
}

Это все по прежнему на этапе компиляции разворачивалось в поле и два метода, но нам приходилось писать намного меньше. А наличие сниппета prop позволяет вообще набрать 4 символа и нажать Tab.
Не знаю как в WinForms, но в WPF весь Binding построен именно на свойствах, т.е. выполнить Binding к свойству - можно, к полю нет.
На этом со свойствами заканчиваю, единственно вот здесь можно посмотреть про DependencyProperty, если еще не в курсе что это такое и зачем оно нужно при биндинге.
Все, на этом экскурс заканчиваю, перехожу к теме.

Медленное чтение
Т.к. свойства это два метода, но благодаря тому, что они выглядят как поля в процессе использования, обычно практикуется подход, в котором свойства выполняются быстро. Т.е. вы пытаетесь считать и тут же получаете значение. Особенно это актуально при Binding-е т.к. если у вас на форме несколько свойств, которые выполняются медленно, то с перерисовкой могут возникнуть проблемы. Для примера, пусть у нас будет вот такой класс:
class Person
{
    public string LastName
    {
        get
        {
            Thread.Sleep(2000);
            return "Иванов";
        }
    }

    public string FirstName
    {
        get
        {
            return "Иван";
        }
    }
}

Здесь Thread.Sleep выполняет функцию имитации длительной операции подготовки данных.
Если создать окно вот с такой разметкой:
<Window x:Class="WpfApplication40.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <TextBlock Text="{Binding FirstName}" />
        <TextBlock Text="{Binding LastName}" />
        <Button Content="Set" Click="Button_Click" />
    </StackPanel>
</Window>

И обработчиком клика по кнопке:
private void Button_Click(object sender, RoutedEventArgs e)
{
    this.DataContext = new Person();
}

Запустив наше приложение и кликнув, мы увидим классический "зависший" интерфейс. Две секунды приложение не будет отвечать ни на какие события. Что делать? К сожалению универсальных решений не существует. Если у нас это только Binding, то мы можем воспользоваться параметром IsAsync, в этом случае сама среда выполнения будет выполнять получение значения в отдельном потоке и у нас все будет хорошо. Но что делать если у нас идет работа из кода? Если данные которое возвращает свойство можно кэшировать, то можно попробовать вот такой вариант:
private string _lastName = null;

public string LastName
{
    get
    {
        if (_lastName == null)
        {
            Thread.Sleep(2000);
            _lastName = "Иванов";
        }
        return _lastName;
    }
}

Первый раз свойство работает медленно, потом быстро. Такое может пригодится, если мы работает с большим  файлом, в котором сохранены некоторые данные. Первый раз обращаясь к свойству сколько наборов данных в файле, свойство "тормозит", т.к. разбирает файл, зато во всех остальных случаях работает быстро. Если у нас нет возможности тормозить основной поток даже при первом обращении или каждое чтение из нашего свойства приводит к длительной операции, то от такого свойства надо отказываться и переходить на async методы.

Медленная запись
Как я уже сказал выше, свойство должно быть быстром. Но что делать, если изменение свойства это медленная операция?
Давайте рассмотрим вот такой класс:

class Person : INotifyPropertyChanged
{
    private string _lastName = null;

    public string LastName
    {
        get
        {
            return _lastName;
        }
        set
        {
            if (value != _lastName)
             {
                _lastName = value;
                SlowOperation(_lastName);
                OnProeprtyChanged("LastName");
            }
        }
    }
 
    void SlowOperation(string p_value)
    {
        // Медленная операция с p_value
        Thread.Sleep(2000);
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
 
    protected void OnProeprtyChanged(string p_propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(p_propertyName));
        }
    }
}

XAML пусть будет тот же, что и в примере выше, а вот код окна поменяем:
 
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        person = new Person();
        DataContext = person;
    }
 
    Person person = null;
 
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        person.LastName = "Иванов";
    }
}

Запускаем приложение, нажимаем кнопку, наблюдаем "зависание", появляется надпись.
Решением в лоб, было бы сделать вот так:
public string LastName
{
    get
    {
        return _lastName;
    }
    set
    {
        if (value != _lastName)
        {
            _lastName = value;
            Task.Factory.StartNew(() => SlowOperation(_lastName));
            OnProeprtyChanged("LastName");
        }
    }
}

Все, подвисание интерфейса исчезло, значение появляется мгновенно. Но, чтобы было понятно, к чему может привести такое решение "в лоб" давайте чуть усложним пример. Добавим в класс поле и нашу SlowOperation озадачим "реальной" работой с этим полем:

int _temp = 0;

void SlowOperation(string p_value)
{
    int i = 100000000;
    while (i > 0)
    {
        _temp++;
        _temp--;
        i--;
    }
}

Понятно, что если все выполняется нормально, то в поле _temp всегда будет 0. Но, давайте поравим код нашей кнопки, чтобы присвоения значения шли быстрее, чем успевает отработать SlowOperation:

private void Button_Click(object sender, RoutedEventArgs e)
{
    for (int i = 0; i < 10; i++)
    {
        person.LastName = i.ToString();
    }           
}

Запускаем, нажимаем кнопку, немного ждем, в обработчик кнопки ставим точку останова (ну лень мне выводить поле), и смотри что у нас там в _temp (Можно кликнуть на картинке и посмотреть покрупнее):
От куда там взялось такое число? Да, это классические гонки.
Ок, дорабатываем наш код, чтобы у нас медленные операции выполнялись в отдельном потоке, но последовательно. В принципе, для этого мы можем обойтись простейшей блокировкой. Например, так:

int _temp = 0;
 
object locker = new object();
 
void SlowOperation(string p_value)
{
    lock(locker)
    {
        int i = 100000000;
        while (i > 0)
        {
            _temp++;
            _temp--;
            i--;
        }
    }
}

Запускаем по уже описанной схеме:
Как видно, проблема гонок устранена. 

Комментариев нет:

Отправить комментарий