четверг, 14 октября 2010 г.

На злобу дня

Некоторые люди создают совершенно глупые системы, которые якобы не позволяют выносить внутрикорпоративную информацию из офиса. Где-то такие системы могут и работать, но уж точно не в обществе программистов.
Что самое печальное — одна из таких систем внедрена на моей текущей работе. После того, как «Dropbox» продолжил отлично работать вместе с этой системой защиты, относительно её качества у меня не осталось никаких сомнений.
Но кроме своей полной бесполезности система имеет свойство доставлять некоторые неприятности. Например, при выборе файла для отправки через браузер вылезает предупреждение о том, что так поступать нехорошо и приходится убивать процесс. Жутко неудобно. При этом, если вбивать сразу полный путь, то всё работает великолепно. Но заниматься строковой магией вручную противно.
И пока я не разберусь, как же система перехватывает обращения к диалогу выбора файла родилось временное решение: добавить действие копирование полного адреса в контекстное меню файла.

Для этих целей была написана жутко длинная и крайне сложная программа
class Program
{
  [STAThread]
  public static void Main(string[] args)
  {
    if(args != null && args.Any())
    {
      Clipboard.SetText(string.Join(string.Empty,args));
    }
  }
}
А дальше пошли разбирательства с расширениями Windows Shell. Следует сказать, что дела с ним раньше я особо не имел, поэтому делал всё максимально рабоче-крестьянски. По быстрому ознакомившись со статьёй на мсдне в реестре были создан ключ HKEY_CLASSES_ROOT\*\Shell\CopyPath\Command.
В значение HKEY_CLASSES_ROOT\*\Shell\CopyPath лежит красивое название для пункта меню.
В HKEY_CLASSES_ROOT\*\Shell\CopyPath\Command нахожится G:\CopyItemsPath.exe "%1". То есть вначале путь до программы, а потом аргумент, который отвечает за путь до выбранного файла.

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

вторник, 12 октября 2010 г.

«Что есть null» или «Как нам дальше жить»

Мне довольно часто приходится писать бизнес-приложения на объектно ориентированных языках достаточно высокого уровня, например C# или Java. Всё нижесказанное не относится к языкам вроде С++.

За время использования C# у меня успело выработаться довольно строгое отношение к null: встреча с этим значение означает ошибку. Практически всегда.
Если в списке нет элементов, то это должен быть пустой список, а не null.
Если мы вызываем какой-то метод объекта, то null в качестве представителя определённо является ошибкой.
Передача null внутрь метода, который не умеет обрабатывать подобное значение тоже приведёт к ошибке.
От постоянных
if(arg == null)
{
  throw new ArgumentNullException("arg");
}
начинают уставать глаза.
Крайне редко получается, что null являлся одним из значений, при этом не выделяясь среди прочих. Чаще всего, он предполагает какое-то специфические действия.

При этом, ошибки связанные с тем, что что-то не было инициализировано а куда-то пришёл null в качестве недопустимого аргумента периодически возникают. Частично этой проблеме помогают контракты и программист может знать, куда можно передавать null а куда нельзя напрягаясь немного меньше.
Но ведь мы, программисты, люди ленивые и напрягаться не хотим вообще, не так ли? Может быть просто стоит запретить null как значение? Это уже сделано например в Haskell.

Но как же быть тогда с методами, которые предполагают использование null? Нельзя же просто от них отказаться?
Для решения этой проблемы умные люди уже давно завели специальный тип. Можно его описать примерно вот так
public class Maybe<T>:IEquatable<Maybe<T>>
{
  /// <summary>
  ///   Default empty instance.
  /// </summary>
  public static readonly Maybe<T> Empty;
  
  /// <summary>
  ///   Instance constructor.
  /// </summary>
  public Maybe(T value);
  
  /// <summary>
  ///   Gets the underlying value, if it is available
  /// </summary>
  public T Value { get; }
  
  /// <summary>
  ///   Gets a value indicating whether this instance has value.
  /// </summary>
  public bool HasValue { get; }
}
Это очень похоже на стандартную Nullable обёртку, для типов передающихся по значению. На самом деле Empty и есть null, разница лишь в том, что он строго типизирован и совершать недопустимые операции с ним немногим сложнее.
На первых взгляд подобное введение сродни обмену шила на мыло, но всё-таки мы получили одно важное преимущество:
Появятся целые участки кода, в которых Maybe не будет участвовать. Цепочки методов, которые не принимают null и не возвращают его. Теперь никто не передаст туда это значение по ошибке, не вызовет метод с недопустимым параметром или не вернёт null из-за желания поставить на будущее заглушку.
Код сам по себе стал чуть более правильным. Возможно, даже чуточку быстрее (не придётся проводить дополнительные проверки, поскольку отсутствие null будет гарантированно системой типов). К сожалению, подобные конструкции продолжают плодить кучу ветвлений, которые затрудняют читабельность. Давайте попытаемся повысить удобство.

От ветвлений никуда не уйдёшь, если необходимы две ветви вычисления, но ситуация меняется кардинальным образом если есть только одна ветвь. Расширим определение класса Maybe методом
public IEnumerable<T> AsEnumerable()
{
  if (HasValue)
  {
    yield return Value;
  }
}
Это даёт возможность работать с элементов Maybe, как с последовательностью. При этом значению Empty будет соответствовать пустая последовательность, а значимому выражению последовательность из одного элемента.

Maybe<int> delay = GetDelayForOperation(/*  */);
delay.AsEnumerable().ForEach(x => Thread.Sleep(x));
Кстати, а почему-бы не сделать и сам Maybe наследником IEnumerable? Это немного укротит запись, сделает её более читабельной. Кстати, умные люди тоже додумались до такого способа, хоть и предложили решить его несколько иначе.
Но, лично мне, не нравится написание огромного количества лямбд. Кроме того, это не очень удобно, при использовании нескольких аргументов.
Maybe<int> delay1 = GetDelayForOperation(/*  */);
Maybe<int> delay2 = GetDelayForOperation(/*  */);
delay.ForEach(x => delay2.ForEach(y => Thread.Slep(Math.Max(x,y))))
Согласитесь, понять с первого раза что делает эта конструкция довольно сложно. Хотя, я думаю, кто-то уже увидел тут приевшиеся цепочки вычислений и функцию bind.
К счатью, в C# есть так называем язык запросов, который позволяет писать SQL-подобные выражения. Для того, чтобы ипользовать эту возможность понадобитья ещё один класс и немого тёмной магии
public static class Maybe
{
  public static Maybe<V> SelectMany<T, U, V>(this Maybe<T> source, Func<T, Maybe<U>> k, Func<T, U, V> s)
  {
    if (!source.HasValue)
      return Maybe<V>.Empty;
    var u = k(source.Value);
    if (!u.HasValue)
      return Maybe<V>.Empty;
    return s(source.Value, u.Value).ToMaybe();
  }
}
Подобная конструкция позволит записывать жутко страшные выражения весьма компактно. Например вместо
result = null;
var d = GetDirectory();
if(d != null)
{
  var f = GetFile(d);
  if(f != null)
  {
    var lines = GetFileLines(f,d);
    if(lines != null)
    {
      result = lines.Count();
    }
  }
}
можно писать
var result = from d in GetDirectory()
       from f in GetFile(d)
       from lines in GetFileLines(f,d)
       select lines.Count();
И главное, никаких лямбд. На самом деле они просто скрыты за этой языковой конструкцией, но для восприятия это не имеет значения.

Бочкой дёгтя в этой ложке мёда можно считать то, что в настоящее время нет средств чтобы запретить использование null в языке вроде c#, поэтому отказ от null и использование Maybe может быть только на уровне негласной договорённости или правила для StyleCop.
На данный момент я пытаюсь сделать совсем маленький проект, следуя подобной договорённости. Ощущения пока положительные, но для чего-то более масштабного, как мне кажется, необходима поддержка на уровне языка.

Есть ещё несколько маленьких удобств, которые облегчают кодирование и отладку, но они не стоят того, чтобы о ни упоминать. Если хотите, посмотрите более подробно в исходнике.