Помощники на каждый день

Некоторые задачи встречаются при написании кода так часто, что инструменты для их решения должны быть особенно удобны. И если стандартные инструменты языка/платформы разработки кажутся неудовлетворительными, то программист пишет свои решения, которые, в силу частоты использования, становятся его любимыми велосипедами. Поэтому первое, что он делает при смене языка/платформы, это ищет местные аналоги своих решений, либо возможности их воспроизвести.

Представляю свою коллекцию помощников для решения рутинных задач, сложившуюся после миграции с C++ Builder на C#, WPF.

Первая тройка


  public static class IComparableExtensions {
    public static T Minv<T>(this T value, T maxValue) where T : IComparable<T> {
      if (value.CompareTo(maxValue) >= 0) return maxValue;
      return value;
    }
    public static T Maxv<T>(this T value, T minValue) where T : IComparable<T> {
      if (value.CompareTo(minValue) <= 0) return minValue;
      return value;
    }
    public static T Limit<T>(this T value, T minValue, T maxValue) where T : IComparable<T> {
      if (value.CompareTo(minValue) <= 0) return minValue;
      if (value.CompareTo(maxValue) >= 0) return maxValue;
      return value;
    }
  }

Чем же мне оказались неудобными стандартные Math.Min и Math.Max?

1. Необходимостью использовать имя класса Math перед Min и Max. Это настолько раздражало при работе с кодом, содержащем большое количество этих функций, что я переопределял их внутри класса.

2. Необходимостью использовать имя класса Math перед Min и Max и неудобством из-за коцептуального ощущения, что этим функциям не место в этом классе. Какие-либо другие функции Math требовались мне только для работы с геометрией, а вот Min и Max — это счетчики и индексы, это наше всё! И описание методов выше очевидно это показывает.

3. Стандартная нотация функций Min и Max при большой вложенности кажется мне недостаточно читабельной. Я бы предпочел иметь бинарные операторы min и max. Определенные выше методы являются наибольшим приближением к желаемому.

И в заключение, мне всегда требуется усилие, чтобы сообразить, какую функцию нужно использовать для ограничения сверху или снизу. Метод Limit избавляет меня от этих затруднений.

В C# 6 можно написать using System.Math;, и использовать функции без префикса. Спасибо, но поздно.

StringMaker


Для отладки или выдачи в лог часто требуется просто перечислить некоторые значения через пробел.
И для этого можно написать метод вроде void OutDebug(params object[] args) с простой логикой внутри. Но когда классов с таким методом становится несколько, требуется другое решение.

  public class SM {
    StringBuilder Sb;
    SM() { Sb = new StringBuilder(); }
    SM Add(object value) {
      if (value==null) return this;
      var objects = value as IEnumerable;
      if (objects!=null) {
        foreach (var obj in objects) Add(obj);
      } else Sb.Append(value.ToString());
      return this; 
    }
    public override string ToString() { return Sb.ToString(); }
    public static implicit operator string(SM value) { 
      return value==null ? null : value.ToString(); 
    }
    public static SM operator +(SM a, object b) { return a.Add(b); }
    public static SM operator -(SM a, object b) { Sb.Append(' '); return Add(b); }
    public static SM New { get { return new SM(); } }
  }

  public static class IEnumerableExtensions {
    public static IEnumerable Sep(this IEnumerable objects, object separator) {
      bool first = true;
      foreach (var obj in objects) {
        if (first) first = false;
        else yield return separator;
        yield return obj;
      }
      yield break;
    }
  }

Используем:

  var sm = SM.New+"Числа"-1-2-3;
  var rr = new int[] { 1, 2, 3 }; 
  sm = sm+" ("+rr.Sep(", ")+')';
  Trace.WriteLine(sm);

В C# 6 появилась возможность вписывать аргументы внутрь строки. Спасибо, но поздно.

TimerTask


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

В WPF это можно сделать непосредственно:

Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { }));

А можно оформить код, как итератор, и исполнить его с помощью следующего класса:

  public class TimerTask {
    public bool IsPaused, IsCancelled;
    DateTimeOffset NextTime;
    TimeSpan Interval;
    Func<bool> Func;

    static DispatcherTimer Timer;
    static List<TimerTask> TaskList;

    TimerTask (double interval, double delay, Func<bool> func) {
      if (TaskList==null) {
        TaskList = new List<TimerTask>();
        Timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(0.02), IsEnabled = true };
        Timer.Tick += Timer_Tick;
      }
      TaskList.Add(this);
      Interval = TimeSpan.FromSeconds(interval);
      NextTime = DateTimeOffset.Now+TimeSpan.FromSeconds(delay);
      Func = func;
    }
    static void Timer_Tick(object sender, EventArgs ea) {
      int i = 0, cnt = TaskList.Count;
      while (i<cnt) {
        if (TaskList[i].IsCancelled) { TaskList.RemoveAt(i); cnt--; } 
        else { TaskList[i].Tick(); i++; }
      }
    }
    void Tick() {
      if (IsPaused || DateTimeOffset.Now<NextTime) return;
      IsCancelled = !Func();
      NextTime = DateTimeOffset.Now+Interval;
    }
    public static TimerTask DoOnce(Action action, double delay) {
      return new TimerTask(0, delay, () => { action(); return false; });
    }
    public static TimerTask DoForever(Action action, double interval, double delay) {
      return new TimerTask(interval, delay, () => { action(); return true; });
    }
    public static TimerTask DoWhile(Func<bool> func, double interval, double delay) {
      return new TimerTask(interval, delay, () => { return func(); });
    }
    public static TimerTask DoEach(IEnumerable<object> enumerable, double interval, double delay) {
      var enumerator = enumerable.GetEnumerator();
      return new TimerTask(interval, delay, () => { return enumerator.MoveNext(); });
    }
  }

Используем:

  public partial class MainWindow : Window {
    public MainWindow() {
      InitializeComponent();
      TimerTask.DoEach(Start(), 0, 0);
    }
    IEnumerable<object> Start() {
      Title = "Starting 1";
      yield return null;
      Starting1();
      Title = "Starting 2";
      yield return null;
      Starting2();
      Title = "Starting 3";
      yield return null;
      Starting3();
      Title = "Started";
      yield break;
    }
  }

Также этот класс может использоваться для реализации прогресс-диалогов.

Очевидным недостатком данного класса является то, что вызов метода, подобного ShowDialog, в одном из заданий, блокирует исполнение и всех других. Этого бы не было, если бы каждое задание имело собственный экземпляр DispatcherTimer.

IntRangeNotifyCollection


Этот класс решает задачу, которая точно не является частой, но бывает очень важной для программ, отображающих большое количество данных. Если ListBox с виртуализацией показывает лишь 40 записей из коллекции в 100 000, естественно решить, что настоящих записей достаточно иметь только эти 40. Также уведомлять контрол об изменениях в коллекции желательно только в случае необходимости.

То есть, в качестве ItemsSource нужно подставить не настоящую коллекцию, а какой-то другой класс. Самый легковесный, какой только может быть.

  public class IntRangeEnumerator : IEnumerator {
    int _Current, _Last;
    public IntRangeEnumerator(int count) : this(0, count) { }
    public IntRangeEnumerator(int start, int count) { _Current = start-1; _Last = start+count; }
    public object Current { get { return _Current; } }
    public bool MoveNext() { _Current++; return _Current<_Last; }
    public void Dispose() { }
    public void Reset() { }
  }

  public class IntRange : IList {
    int _Start, _Count;
    public IntRange(int count) : this(0, count) { }
    public IntRange(int start, int count) { _Start = start; _Count = count; }
    public int Count { get { return _Count; } }
    public IEnumerator GetEnumerator() { return new IntRangeEnumerator(_Start, _Count); }
    public object this[int index] { get { return _Start+index; } set { } }

Другие методы
    public bool IsSynchronized { get { return true; } }
    public object SyncRoot { get { return this; } }
    public void CopyTo(Array array, int index) {
      for (int i = 0; i<_Count; i++) array.SetValue(_Start+i, index+i);
    }
    public bool IsFixedSize { get { return true; } }
    public bool IsReadOnly { get { return true; } }
    public int Add(object value) { return 0; }
    public void Clear() { }
    public bool Contains(object value) {
      if (!(value is int)) return false;
      int i = (int)value;
      return i>=_Start && i<_Start+_Count; 
    }
    public int IndexOf(object value) {
      if (!(value is int)) return -1;
      int i = (int)value;
      return i>=_Start && i<_Start+_Count ? i-_Start : -1; 
    }
    public void Insert(int index, object value) { }
    public void Remove(object value) { }
    public void RemoveAt(int index) { }


  }

  public class IntRangeNotifyCollection : IEnumerable, INotifyCollectionChanged {
    int _Count;
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    public IntRangeNotifyCollection() { }
    public IEnumerator GetEnumerator() { return new IntRangeEnumerator(_Count); }
    protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) {
      if (CollectionChanged!=null) CollectionChanged(this, e);
    }
    public int Count { 
      get { return _Count; }
      set {
        if (value==_Count) return;
        NotifyCollectionChangedEventArgs e;
        if (value==0) {
          e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
        } else
        if (value>_Count) {
          e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, 
            new IntRange(_Count, value-_Count), _Count);
        } else {
          e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, 
            new IntRange(value, _Count-value), value);
        }
        _Count = value;
        OnCollectionChanged(e);
      }
    }
  }

Как затем связать индексы с записями? Это уже сильно зависит от задачи.

Публикации на эту тему на Хабре:
» Функциональность с Range в ObservableCollection
» Виртуализация данных в WPF

Похожие публикации

Средняя зарплата в IT

111 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 7 185 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 29

    +2
    А чем String.Join хуже вашего велосипеда?
      0
      Тоже возни такой вопрос.
      Неужели такой код:
        var s = string.Join(" ", "Числа", 1, 2, 3) + 
         " (" + string.Join(", ", 1, 2, 3) + ")";
        Trace.WriteLine(s);
      

      хуже чем:
        var sm = SM.New+"Числа"-1-2-3;
        var rr = new int[] { 1, 2, 3 }; 
        sm = sm+" ("+rr.Sep(", ")+')';
        Trace.WriteLine(sm);
      
        –2
        В строке
        var sm = SM.New+"Числа"-1-2-3;
        

        не используются скобки и запятые, экономятся нажатия пальцев!
          +7
          Зато уменьшается читабельность. Сходу и не поймешь, что тут и операторы перегружены, и как они работают.
            0
            кстати String.Join находится в mscorelib, и вы думаете изначально нативный оптимизированный код медленнее вашего велосипеда?
            +2
            Вы это серьезно? ради интереса производительность замерьте, вашего и стандартного.
          +1

          Надо писать String.Join. В C# 6 сделали возможность писать using System.String, спасибо, но поздно.

            0

            Вот только не using System.String, а using static System.String. У автора в статье такая же ошибка: using static System.Math.

              0

              Все равно очень поздно!

                0
                Чем поздно, вы тоже поклонник велосипедов и костылей как автор?
                  0

                  Шутка же.

                    0
                    Табличку «Сарказм» забыли, ибо тут топикстартер с такой серьезностью говорит за экономию нажатий пальцев, даже путаешь, где серьезно, а где нет
          +7
          Спасибо, но поздно.

          Не "поздно", а самое время выкинуть велосипеды, упрощая код.


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

          TPL, Rx.net?


          public static class IEnumerableExtensions {
              public static IEnumerable Sep(this IEnumerable objects, object separator) {
                bool first = true;
                foreach (var obj in objects) {
                  if (first) first = false;
                  else yield return separator;
                  yield return obj;
                }
                yield break;
              }
            }
          
            var rr = new int[] { 1, 2, 3 }; 
            rr.Sep(", ")

          Здравствуй, боксинг.

            0
            Про string.Join уже написали.
            Велосипед с TimerTask заменяется на
            async Task Start() {
                  await Task.Delay(100500);
                  Title = "Starting 1";
                  await Task.Delay(1000);
                  Starting1();
                  await Task.Delay(1000);
                  ///...
            }
            

            Да еще и с поддержкой отмены через CancellationToken. И даже можно сделать опрашивающий прогресс бар.
            async Task Start() {
                  while(!isDone)
                 {
                        UpdateProgressBar();
                        await Task.Delay(1000)
                 }
            }
            

            IntRangeNotifyCollection — прямо помощник на каждый день.
              +5
              Мне вообще непонятно вот это «Спасибо, но поздно»… Что значит поздно? Давайте и async \ await не использовать — поздно же.
                –10
                У меня уже есть решение, которым я полностью удовлетворен. Потребуется веский повод, чтобы перейти на появившийся в языке функционал.
                  +5
                  Избавление от велосипедов, которые полностью покрываются нативными средствами языка (платфомы) это очень веский повод. Мы, конечно, говорим о новых проектах, а не о переписывании существующих. Там надо отдельно анализировать. Но начинать новые продукты с велосипедом, который реализован нативно — имхо неразумно.

                  Если мне приходится обращаться к старым проектам и это разумно — первое что я делаю — быстренько пробегаюсь и заменяю вещи по подсказкам решарпера (типа «В C# 6 появилась возможность вписывать аргументы внутрь строки»). Это недолго даже на больших проектах. Или .? Или nameof,… да многое. Код становится читабельнее, что есть просто великолепно.

                  Старый код? Ок. Нельзя проапгрейдить? Ок. Но «спасибо поздно» — это… неразумно, имхо.
                    0
                    А наличие велосипеда, веская причина им пользоваться? Вам знакомо слово рефакторинг?
                  +2
                  На всякий случай пиарну свой пакет с велосипедом «на каждый день»
                  Kонвертация из А -> B одним методом. Конвертер сам «находит» подходящий способ конвертации и дергает его.
                    0
                    Спасибо, но
                    всё это уже поддерживается средствами языка, работает быстрее и часто выглядит более читаемо…
                      0

                      Настораживает необходимость частого использования метода Limit в приложении.
                      Представляется нечто такое.


                      var fixedValue = badValue.Limit(MinVal, MaxVal);

                      Само наличие этого простого в использовании метода может породить вторую проблему — костыли.

                        0
                        Когда то я тоже создавал подобные велосипеды, но потом пришло понимание, что нужно пользоваться возможностями языка и стандартной библиотеки платформы, или же библиотеками-фреймворками — стандартами де-факто.

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

                        Однако, опыт разработки своих велосипедов помогает и хорошему — оттачивает навык разработки по SOLID, и позволяет научиться разрабатывать не приложения, а библиотеки/фреймворки, которыми приложения пользуются.
                        Другое дело, в большинстве случаев прикладные программисты разрабатывают приложения, фичи приложений, но не библиотеки, а разработка приложения в виде набора независимых компонентов может быть сочтена руководством оверхедом.
                          0
                          И еще один момент: как при работе в команде вы будете убеждать участников пользоваться своими велосипедами?
                          А что, если у других тоже есть свои велоcипеды?

                          Кстати, в том числе поэтому я и полагаю, что полноценная развитая платформа разработки обязана иметь богатую стандартную библиотеку практически на все случаи жизни, и язык должен иметь достаточно сахара, чтобы языковые конструкции можно было записывать кратко.
                            +1
                            Приведенные велосипеды во многом еще и являются образчиками «как не надо делать».

                            Например:
                            public static SM New { get { return new SM(); } }

                            Обращение к свойству не должно создавать новый объект, если это только не Lazy-инициализация свойства.
                            Также свойство при повторном вызове должно возвращать то же самое значение, если между обращениями к свойству не вызывались методы, меняющие состояние объекта.

                            В вашем случае нужно не свойство SM New, а метод SM New(), или — зачем метод? — чем не подходит создание объекта через конструктор — new SM()?

                            Рекомендую почитать Рихтера и ознакомиться с различными best practices для C#.
                            А то вы не только создаете велосипеды, но и еще и пытаетесь натянуть на C# синтаксис и подходы/практики языков, с которыми раньше работали.

                            Вопрос на засыпку:
                            как нужно доработать StringMaker, чтобы он мог форматировать строки не только в текущей культуре? с учетом того, что не все объекты, которые вы добавляете в SM, могут поддерживать форматирование с учетом культуры.
                              0
                              Метод это же писать аж целых две скобки, а через конструктор, это еще и new писать.
                                –2
                                Именно так. На C++ это был макрос, просто 'SM'.
                                  +1
                                  Вы вообще не понимаете, что это плохо? производительность, плохой стиль, нарушение правил перегрузки операторов, просто ради меньше писать? Или вы табличку сарказм забыли. Вы по минусам это не поняли? Переименуйте статью, как не надо писать на С#. будет ближе к истине.
                                  0

                                  он и так написал new. Только два раза — сначала в определении метода. Скобку надо писать одну — Вторую IDE дописывает :)

                                0
                                А почему у вас класс именуется как интерфейс? Не по канонам.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое