Изначально при реализации сопоставления с образцом мне хотелось сделать синтаксис case-выражения похожим на следующий:
s => string.IsNullOrEmpty(s) => 0
К сожалению, в C# это является синтаксически неверным: по сути
s => t => s * t
Представляет собой функцию в каррированой форме. Второй идеей для case выражения было использование тернарного оператора вроде следующего:
s => t ? a : b
Которое опять-таки невозможно по причине отсутствия в C# типа Unit (для использования в ветке else). Была идея типом для выражения b сделать Expression<..> и передавать туда следующий case, но этому препятствует требование идентичности типов для выражений a и b.
В какой-то момент я свыкся с мыслью, что реализовать контекстную связанность мне не удастся и пользовался мэтчингом в том виде, в котором он есть.
Однажды в процессе отладки кода, вроде следующего
... {s => s is string, s => ((string)s).Length} ...
я подумал, что вместо проверки типа is можно проводить преобразование as и проверять результат этого преобразования. Тут меня осенило — ведь это же и будет по сути построением контекста! Не откладывая надолго, я взялся за реализацию.
Во второй версии решено было отказаться совсем от реализации Matcher перебором лямбда-функций и использовать только деревья выражений (как в ExprMatcher). Метод Add пришлось сделать типизированым:
public void Add<TCtx>(Expression<Func<TIn, TCtx>> binder, Expression<Func<TCtx, TOut>> processor) { var bindResult = Expression.Variable(typeof (TCtx), "binded"); var caseExpr = Expression.Block( new []{bindResult}, Expression.Assign(bindResult, Expression.Invoke(binder, Parameter)), Expression.IfThen( Expression.NotEqual(Expression.Convert(bindResult, typeof(object)), Expression.Constant(null)), Expression.Return(RetPoint, Expression.Invoke(processor, bindResult)) )); _caseExpressionsList.Add(caseExpr); }
Тип TCtx является «типом контекста» для кейса. В случае, если первое выражение вернуло не null экземпляр TCtx, выполняется второе выражение, причём аргументом для него является результат сопоставления.
Предыдущий синтаксис с предикатами решено было оставить, т.к. он иногда удобнее:
public void Add<TCtx>(Expression<Func<TIn, TCtx>> binder, Expression<Func<TCtx, TOut>> processor) { var bindResult = Expression.Variable(typeof (TCtx), "binded"); var caseExpr = Expression.Block( new []{bindResult}, Expression.Assign(bindResult, Expression.Invoke(binder, Parameter)), Expression.IfThen( Expression.NotEqual(Expression.Convert(bindResult, typeof(object)), Expression.Constant(null)), Expression.Return(RetPoint, Expression.Invoke(processor, bindResult)) )); _caseExpressionsList.Add(caseExpr); }
Т.к. выражения для кейсов теперь строятся непостредственно при добавлении, код сборки полного выражения мэтчера существенно упростился:
private Func<TIn, TOut> CompileMatcher() { var finalExpressions = new Expression[] { Expression.Throw(Expression.Constant(new MatchException("Provided value was not matched with any case"))), Expression.Label(RetPoint, Expression.Default(typeof(TOut))) }; var matcherExpression = Expression.Block(_caseExpressionsList.Concat(finalExpressions)); return Expression.Lambda<Func<TIn, TOut>>(matcherExpression, Parameter).Compile(); }
Приведу небольшой пример функции, возвращающей значение аргумента, в случае если его тип — string или StringBuilder и строку «Unknown object» — в ином случае:
var match = new Matcher<object, string> { {s => s as string, s => s}, {sb => sb as StringBuilder, sb => sb.ToString()}, {o => true, (bool _) => "Unknown object"} }.ToFunc();
В дальнейших планах — добавить в пакет набор готовых дискриминаторов для часто встречающихся кейсов.
На этом, пожалуй, всё. На всякий случай приведу ссылки на проект и nuget-пакет мэтчера:
Проект на bitbucket
Nuget пакет
Как и в прошлый раз, буду рад замечаниям/предложениям в комментариях.
Спасибо за внимание!
ссылка на оригинал статьи http://habrahabr.ru/post/227935/
Добавить комментарий