Сегодня утром мой приятель kirillkos столкнулся с проблемой.
Проблемный код
Вот его код:
class Event { public string Message {get;set;} public DateTime EventTime {get;set;} } interface IEventProvider { IEnumerable<Event> GetEvents(); }
И дальше много-много реализаций IEventProvider, достающие данные из разных таблиц и баз.
Проблема: во всех этих базах все в разных временных зонах. Соответственно, при попытке вывести события на UI все ужасно перепутано.
Слава Хейлсбергу, у нас есть типы, пусть они спасут нас!
Попытка 1
class Event { public string Message {get;set;} public DateTimeOffset EventTime {get;set; } }
DateTimeOffset замечательный тип, он хранит информацию о смещении относительно UTC. Он прекрасно поддерживается MS SQL и Entity Framework (а в версии 6.3 будет поддерживаться еще лучше). У нас в code style он обязательный для всего нового кода.
Теперь мы можем собрать информацию с этих самых provider и консистентно, полагаясь на типы, вывести все на UI. Победа!
Проблема: DateTimeOffset умеет неявно преобразовываться из DateTime.
Следующий код прекрасно скомпилируется:
class Event { public string Message {get;set;} public DateTimeOffset EventTime {get;set; } } IEnumerable<Event> GetEvents() { return new[] { new Event() {EventTime = DateTime.Now, Message = "Hello from unknown time!"}, }; }
Это потому, что у DateTimeOffset определен оператор неявного приведения типов:
// Local and Unspecified are both treated as Local public static implicit operator DateTimeOffset (DateTime dateTime);
Это совсем не то, что нам нужно. Мы-то хотели, чтобы программист при написании кода был вынужден задуматься: «а в какой собственной временной зоне случилось это событие? Откуда взять зону?». Часто совсем из других полей, иногда из связанных таблиц. А тут совершить ошибку не задумавшись очень легко.
Проклятые неявные преобразования!
Попытка 2
С тех пор, как я услышал про молоток статические анализаторы, мне все кажется гвоздями подходящими случаями для них. Нам надо написать статический анализатор, который запрещает это неявное преобразование, и объясняет почему… Выглядит, как многовато работы. Да и вообще, это работа компилятора, проверять типы. Пока отложим эту идею, как многословную.
Попытка 3
Вот если мы были бы в мире F#, сказал kirillkos.
Мы бы тогда:
type DateTimeOffsetStrict = Value of DateTimeOffset
И дальше не придумал импровизируй какая-то магия нас спасла бы. Жаль, что у нас в конторе не пишут на F#, да и мы с kirillkos его толком не знаем :-)
Попытка 4
Неужели что-то такое нельзя сделать на C#? Можно, но замучаешься преобразовывать туда-сюда. Стоп, но ведь мы только что видели, как можно сделать неявные преобразования!
/// <summary> /// Same as <see cref="DateTimeOffset"/> /// but w/o implicit conversion from <see cref="DateTime"/> /// </summary> public readonly struct DateTimeOffsetStrict { private DateTimeOffset Internal { get; } private DateTimeOffsetStrict(DateTimeOffset @internal) { Internal = @internal; } public static implicit operator DateTimeOffsetStrict(DateTimeOffset dto) => new DateTimeOffsetStrict(dto); public static implicit operator DateTimeOffset(DateTimeOffsetStrict strict) => strict.Internal; }
Самое интересное в этом типе, что он неявно преобразуется туда-сюда из DateTimeOffset, а вот попытка неявно преобразовать его из DateTime вызовет ошибку компиляции, преобразования из DateTime возможны только явные. Компилятор не может вызвать «цепочку» неявных преобразований, если они определены в нашем коде, это ему запрещает стандарт (цитата на SO). То есть, вот так работает:
class Event { public string Message {get;set;} public DateTimeOffsetStrict EventTime {get;set; } } IEnumerable<Event> GetEvents() { return new[] { new Event() {EventTime = DateTimeOffset.Now, Message = "Hello from unknown time!"}, }; }
а вот так нет:
IEnumerable<Event> GetEvents() { return new[] { new Event() {EventTime = DateTime.Now, Message = "Hello from unknown time!"}, }; }
Что нам и требовалось!
Итог
Пока не знаем, будем ли внедрять. Только всех приучили к DateTimeOffset, и теперь его заменять на наш тип — стремновато. Да и наверняка всплывут проблемы на уровне EF, ASP.NET parameter binding и еще в тысяче мест. Но самое решение кажется мне интересным. Аналогичные трюки я использовал, чтобы следить за безопасностью пользовательского ввода — делал тип UnsafeHtml, который неявно преобразуется из строки, а вот обратно его преобразовать в строку или IHtmlString можно только путем вызова sanitizer.
