[Из песочницы] Помощники на каждый день
Представляю свою коллекцию помощников для решения рутинных задач, сложившуюся после миграции с C++ Builder на C#, WPF.
Первая тройка
public static class IComparableExtensions {
public static T Minv(this T value, T maxValue) where T : IComparable {
if (value.CompareTo(maxValue) >= 0) return maxValue;
return value;
}
public static T Maxv(this T value, T minValue) where T : IComparable {
if (value.CompareTo(minValue) <= 0) return minValue;
return value;
}
public static T Limit(this T value, T minValue, T maxValue) where T : IComparable {
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 Func;
static DispatcherTimer Timer;
static List TaskList;
TimerTask (double interval, double delay, Func func) {
if (TaskList==null) {
TaskList = new List();
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 { 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 func, double interval, double delay) {
return new TimerTask(interval, delay, () => { return func(); });
}
public static TimerTask DoEach(IEnumerable
Используем:
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
TimerTask.DoEach(Start(), 0, 0);
}
IEnumerable
Также этот класс может использоваться для реализации прогресс-диалогов.
Очевидным недостатком данного класса является то, что вызов метода, подобного 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
Комментарии (14)
28 октября 2016 в 13:14
0↑
↓
А чем String.Join хуже вашего велосипеда?28 октября 2016 в 13:37
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);
28 октября 2016 в 13:41
0↑
↓
В строкеvar sm = SM.New+"Числа"-1-2-3;
не используются скобки и запятые, экономятся нажатия пальцев!28 октября 2016 в 13:44
+1↑
↓
Зато уменьшается читабельность. Сходу и не поймешь, что тут и операторы перегружены, и как они работают.28 октября 2016 в 14:05
0↑
↓
кстати String.Join находится в mscorelib, и вы думаете изначально нативный оптимизированный код медленнее вашего велосипеда?
28 октября 2016 в 13:46
0↑
↓
Вы это серьезно? ради интереса производительность замерьте, вашего и стандартного.
28 октября 2016 в 13:42
+1↑
↓
Надо писать
String.Join
. В C# 6 сделали возможность писатьusing System.String
, спасибо, но поздно.
28 октября 2016 в 13:27 (комментарий был изменён)
+1↑
↓
Спасибо, но поздно.
Не «поздно», а самое время выкинуть велосипеды, упрощая код.
В среде, управляемой событиями, иногда возникает необходимость продолжить работу после обработки накопившихся событий. Обычно, для отображения промежуточных результатов работы.
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(", ")
Здравствуй, боксинг.
28 октября 2016 в 13:28
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 — прямо помощник на каждый день.28 октября 2016 в 13:28
+1↑
↓
Мне вообще непонятно вот это «Спасибо, но поздно»… Что значит поздно? Давайте и async \ await не использовать — поздно же.28 октября 2016 в 13:45
–1↑
↓
У меня уже есть решение, которым я полностью удовлетворен. Потребуется веский повод, чтобы перейти на появившийся в языке функционал.28 октября 2016 в 13:54
+1↑
↓
Избавление от велосипедов, которые полностью покрываются нативными средствами языка (платфомы) это очень веский повод. Мы, конечно, говорим о новых проектах, а не о переписывании существующих. Там надо отдельно анализировать. Но начинать новые продукты с велосипедом, который реализован нативно — имхо неразумно.Если мне приходится обращаться к старым проектам и это разумно — первое что я делаю — быстренько пробегаюсь и заменяю вещи по подсказкам решарпера (типа «В C# 6 появилась возможность вписывать аргументы внутрь строки»). Это недолго даже на больших проектах. Или .? Или nameof, … да многое. Код становится читабельнее, что есть просто великолепно.
Старый код? Ок. Нельзя проапгрейдить? Ок. Но «спасибо поздно» — это… неразумно, имхо.
28 октября 2016 в 14:12
0↑
↓
А наличие велосипеда, веская причина им пользоваться? Вам знакомо слово рефакторинг?
28 октября 2016 в 13:32
0↑
↓
На всякий случай пиарну свой пакет с велосипедом «на каждый день»
Kонвертация из А → B одним методом. Конвертер сам «находит» подходящий способ конвертации и дергает его.