Представляю свою коллекцию помощников для решения рутинных задач, сложившуюся после миграции с 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)
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(", ")
Здравствуй, боксинг.
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 — прямо помощник на каждый день.
vdasus
28.10.2016 13:28+5Мне вообще непонятно вот это «Спасибо, но поздно»… Что значит поздно? Давайте и async \ await не использовать — поздно же.
AmirYantimirov
28.10.2016 13:45-10У меня уже есть решение, которым я полностью удовлетворен. Потребуется веский повод, чтобы перейти на появившийся в языке функционал.
vdasus
28.10.2016 13:54+5Избавление от велосипедов, которые полностью покрываются нативными средствами языка (платфомы) это очень веский повод. Мы, конечно, говорим о новых проектах, а не о переписывании существующих. Там надо отдельно анализировать. Но начинать новые продукты с велосипедом, который реализован нативно — имхо неразумно.
Если мне приходится обращаться к старым проектам и это разумно — первое что я делаю — быстренько пробегаюсь и заменяю вещи по подсказкам решарпера (типа «В C# 6 появилась возможность вписывать аргументы внутрь строки»). Это недолго даже на больших проектах. Или .? Или nameof,… да многое. Код становится читабельнее, что есть просто великолепно.
Старый код? Ок. Нельзя проапгрейдить? Ок. Но «спасибо поздно» — это… неразумно, имхо.
yarosroman
28.10.2016 14:12А наличие велосипеда, веская причина им пользоваться? Вам знакомо слово рефакторинг?
shai_hulud
28.10.2016 13:32+2На всякий случай пиарну свой пакет с велосипедом «на каждый день»
Kонвертация из А -> B одним методом. Конвертер сам «находит» подходящий способ конвертации и дергает его.
fedorro
28.10.2016 15:30Спасибо, но
всё это уже поддерживается средствами языка, работает быстрее и часто выглядит более читаемо…
ink-shtil
29.10.2016 04:36Настораживает необходимость частого использования метода Limit в приложении.
Представляется нечто такое.
var fixedValue = badValue.Limit(MinVal, MaxVal);
Само наличие этого простого в использовании метода может породить вторую проблему — костыли.
sand14
29.10.2016 04:36Когда то я тоже создавал подобные велосипеды, но потом пришло понимание, что нужно пользоваться возможностями языка и стандартной библиотеки платформы, или же библиотеками-фреймворками — стандартами де-факто.
Свои велосипеды по определению не охватывают всех кейсов, могут быть недостаточно производительны, по мере развития языка или библиотеки платформы теряют актуальность,
и вообще практика тащить в каждый проект свои велосипедные наработки мне кажется плохой, не говоря юридических аспектах.
Однако, опыт разработки своих велосипедов помогает и хорошему — оттачивает навык разработки по SOLID, и позволяет научиться разрабатывать не приложения, а библиотеки/фреймворки, которыми приложения пользуются.
Другое дело, в большинстве случаев прикладные программисты разрабатывают приложения, фичи приложений, но не библиотеки, а разработка приложения в виде набора независимых компонентов может быть сочтена руководством оверхедом.
sand14
29.10.2016 06:44И еще один момент: как при работе в команде вы будете убеждать участников пользоваться своими велосипедами?
А что, если у других тоже есть свои велоcипеды?
Кстати, в том числе поэтому я и полагаю, что полноценная развитая платформа разработки обязана иметь богатую стандартную библиотеку практически на все случаи жизни, и язык должен иметь достаточно сахара, чтобы языковые конструкции можно было записывать кратко.
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, могут поддерживать форматирование с учетом культуры.yarosroman
29.10.2016 10:05Метод это же писать аж целых две скобки, а через конструктор, это еще и new писать.
AmirYantimirov
30.10.2016 08:05-2Именно так. На C++ это был макрос, просто 'SM'.
yarosroman
30.10.2016 09:50+1Вы вообще не понимаете, что это плохо? производительность, плохой стиль, нарушение правил перегрузки операторов, просто ради меньше писать? Или вы табличку сарказм забыли. Вы по минусам это не поняли? Переименуйте статью, как не надо писать на С#. будет ближе к истине.
ApeCoder
31.10.2016 13:51он и так написал new. Только два раза — сначала в определении метода. Скобку надо писать одну — Вторую IDE дописывает :)
yarosroman
А чем String.Join хуже вашего велосипеда?
Vadem
Тоже возни такой вопрос.
Неужели такой код:
хуже чем:
AmirYantimirov
В строке
не используются скобки и запятые, экономятся нажатия пальцев!
denismaster
Зато уменьшается читабельность. Сходу и не поймешь, что тут и операторы перегружены, и как они работают.
yarosroman
кстати String.Join находится в mscorelib, и вы думаете изначально нативный оптимизированный код медленнее вашего велосипеда?
yarosroman
Вы это серьезно? ради интереса производительность замерьте, вашего и стандартного.
AgentFire
Надо писать
String.Join
. В C# 6 сделали возможность писатьusing System.String
, спасибо, но поздно.KvanTTT
Вот только не
using System.String
, аusing static System.String
. У автора в статье такая же ошибка:using static System.Math
.AgentFire
Все равно очень поздно!
yarosroman
Чем поздно, вы тоже поклонник велосипедов и костылей как автор?
AgentFire
Шутка же.
yarosroman
Табличку «Сарказм» забыли, ибо тут топикстартер с такой серьезностью говорит за экономию нажатий пальцев, даже путаешь, где серьезно, а где нет