Помічники на кожен день

Деякі завдання зустрічаються при написанні коду так часто, що інструменти для їх вирішення повинні бути особливо зручні. І якщо стандартні інструменти мови/платформи розробки здаються незадовільними, то програміст пише свої рішення, які, в силу частоти використання, стають його улюбленими велосипедами. Тому перше, що він робить при зміні мови/платформи, це шукає місцеві аналоги своїх рішень, або можливості їх відтворити.

Уявляю свою колекцію помічників для виконання рутинних завдань, що склалася після міграції з 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> перечіслімого, double interval, double delay) {
var enumerator = перечіслімого.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
Цей клас вирішує задачу, яка точно не є частою, але буває дуже важливою для програм, що відображають велику кількість даних. Якщо Список з віртуалізацією показує лише 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);
}
}
}

Як потім зв'язати індекси з записами? Це вже сильно залежить від завдання.

Публікації на цю тему на Хабре:
» Функціональність з Діапазоном ObservableCollection
» Віртуалізація даних в WPF
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.