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

Представляю свою коллекцию помощников для решения рутинных задач, сложившуюся после миграции с 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
Поделиться с друзьями
-->

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


  1. yarosroman
    28.10.2016 13:14
    +2

    А чем String.Join хуже вашего велосипеда?


    1. Vadem
      28.10.2016 13:37

      Тоже возни такой вопрос.
      Неужели такой код:

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


      1. AmirYantimirov
        28.10.2016 13:41
        -2

        В строке

        var sm = SM.New+"Числа"-1-2-3;
        

        не используются скобки и запятые, экономятся нажатия пальцев!


        1. denismaster
          28.10.2016 13:44
          +7

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


          1. yarosroman
            28.10.2016 14:05

            кстати String.Join находится в mscorelib, и вы думаете изначально нативный оптимизированный код медленнее вашего велосипеда?


        1. yarosroman
          28.10.2016 13:46
          +2

          Вы это серьезно? ради интереса производительность замерьте, вашего и стандартного.


    1. AgentFire
      28.10.2016 13:42
      +1

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


      1. KvanTTT
        28.10.2016 15:59

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


        1. AgentFire
          29.10.2016 11:28

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


          1. yarosroman
            29.10.2016 16:44

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


            1. AgentFire
              29.10.2016 18:06

              Шутка же.


              1. yarosroman
                31.10.2016 03:28

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


  1. lair
    28.10.2016 13:27
    +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(", ")

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


  1. shai_hulud
    28.10.2016 13:28

    Про 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 — прямо помощник на каждый день.


  1. vdasus
    28.10.2016 13:28
    +5

    Мне вообще непонятно вот это «Спасибо, но поздно»… Что значит поздно? Давайте и async \ await не использовать — поздно же.


    1. AmirYantimirov
      28.10.2016 13:45
      -10

      У меня уже есть решение, которым я полностью удовлетворен. Потребуется веский повод, чтобы перейти на появившийся в языке функционал.


      1. vdasus
        28.10.2016 13:54
        +5

        Избавление от велосипедов, которые полностью покрываются нативными средствами языка (платфомы) это очень веский повод. Мы, конечно, говорим о новых проектах, а не о переписывании существующих. Там надо отдельно анализировать. Но начинать новые продукты с велосипедом, который реализован нативно — имхо неразумно.

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

        Старый код? Ок. Нельзя проапгрейдить? Ок. Но «спасибо поздно» — это… неразумно, имхо.


      1. yarosroman
        28.10.2016 14:12

        А наличие велосипеда, веская причина им пользоваться? Вам знакомо слово рефакторинг?


  1. shai_hulud
    28.10.2016 13:32
    +2

    На всякий случай пиарну свой пакет с велосипедом «на каждый день»
    Kонвертация из А -> B одним методом. Конвертер сам «находит» подходящий способ конвертации и дергает его.


  1. fedorro
    28.10.2016 15:30

    Спасибо, но
    всё это уже поддерживается средствами языка, работает быстрее и часто выглядит более читаемо…


  1. ink-shtil
    29.10.2016 04:36

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


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

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


  1. sand14
    29.10.2016 04:36

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

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

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


  1. sand14
    29.10.2016 06:44

    И еще один момент: как при работе в команде вы будете убеждать участников пользоваться своими велосипедами?
    А что, если у других тоже есть свои велоcипеды?

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


  1. sand14
    29.10.2016 07:29
    +1

    Приведенные велосипеды во многом еще и являются образчиками «как не надо делать».

    Например:

    public static SM New { get { return new SM(); } }

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

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

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

    Вопрос на засыпку:
    как нужно доработать StringMaker, чтобы он мог форматировать строки не только в текущей культуре? с учетом того, что не все объекты, которые вы добавляете в SM, могут поддерживать форматирование с учетом культуры.


    1. yarosroman
      29.10.2016 10:05

      Метод это же писать аж целых две скобки, а через конструктор, это еще и new писать.


      1. AmirYantimirov
        30.10.2016 08:05
        -2

        Именно так. На C++ это был макрос, просто 'SM'.


        1. yarosroman
          30.10.2016 09:50
          +1

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


      1. ApeCoder
        31.10.2016 13:51

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


  1. dmitry_dvm
    29.10.2016 13:59

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