У вас никогда не возникало ощущения, что в языке X, на котором вы в данный момент программируете чего-то не хватает? Какой-нибудь небольшой, но приятной плюшки, которая может и не сделала бы вашу жизнь абсолютно счастливой, но определенно добавила бы немало радостных моментов. И вот вы с черной завистью посматриваете на язык Y, в котором эта штуковина есть, грустно вздыхаете и тайком льете по ночам слезы бессилия в любимую подушку. Бывало?
Пожалуй, C# дает своим адептам и меньше поводов для такой зависти, в сравнении с многими другими, поскольку динамично развивается, добавляя все новые и новые упрощающие жизнь фичи. И все же, нет предела совершенству, причем для каждого из нас — своего.
Сразу отмечу, что приоритетом в данной работе для меня было желание попробовать на зуб Roslyn, а сама идея, которую я дальше опишу, была скорее поводом и тестовым примером для испытаний этой библиотеки. Однако в процессе изучения и реализации я выяснил, что хоть и с некоторыми бубноплясками, но результат действительно можно использовать на практике для реального расширения синтаксиса языка. Как это сделать, кратко опишу в самом конце. А пока приступим.
Идея безопасных вызовов заключается в том, чтобы избавиться от надоедающих проверок любых классов на null, которые являясь необходимостью, в то же время значительно засоряют код и ухудшают его читабельность. В то же время, нет никакого желания находиться под постоянной угрозой выпадения NullReferenceException.
Данная проблема решена в функциональных языках программирования с помощью монады Maybe, суть которой заключается в том, что после boxing'а использующийся в конвеерных вычислениях тип может содержать некоторое значение, либо значение Nothing. В случае, если предыдущее вычисление в конвеере дало некоторый результат, то производится следующее вычисление, если же оно вернуло Nothing, то вместо следующего вычисления вновь возвращается Nothing.
В С# созданы все условия для реализации данной монады — вместо Nothing используется null, для структурных типов могут использоваться их Nullable<T> версии. В принципе, идея уже витает в воздухе, и было несколько статей, в которых реализовывалась данная монада в C# с помощью LINQ. Одна из них принадлежит Дмитрию Нестеруку mezastel, есть еще и другая.
Но нельзя не отметить, что при всей заманчивости такого подхода, результирующий код с использованием монады выглядит весьма туманно, из-за необходимости использовать вместо прямых вызовов обертки из лямбда-функций и LINQ. Однако без синтаксических средств языка реализовать ее более элегантно вряд ли представляется возможным.
Достаточно элегантный, как мне показалось, способ реализации данной идеи я обнаружил в спецификации еще пока не созданного языка Kotlin для JDK от ребят из горячо мною любимой компании JetBrains (Null-safety). Как оказалось, такой есть уже в Groovy, возможно и еще где-то.
Итак, что же это за оператор безопасного вызова? Предположим, у нас есть выражение:
В случае, если SomeObject является null, мы неминуемо, как уже говорилось, получим NullReferenceException. Чтобы этого избежать, определим в дополнение к оператору прямого вызова '.' еще и оператор безопасного вызова '?.' который выглядит следующим образом:
и представляет собой на самом деле выражение:
В случае, если безопасно вызываемый метод или свойство возвращает структурный тип, необходимо чтобы присваиваемая переменная имела тип Nullable.
Как и обычные вызовы, такие безопасные вызовы можно использовать цепочками, например:
который преобразуется в выражение:
Здесь скрывается некоторый недостаток предлагаемого мной преобразования, поскольку оно порождает дополнительные вызовы функций. На самом деле желательно было бы преобразовывать его, например, к виду:
Однако в ввиду некоторой многословности Roslyn, для того, чтобы примеры не были бы чересчур раздутыми и занудными, я решил сделать преборазование попроще. Впрочем об этом в следующих частях.
Как вы может уже слышали, совсем недавно была выпущена CTP версия проекта Roslyn, в рамках которого разработчиками языков C# и VB были полностью переписаны компиляторы языков с использованием managed кода, и открыт доступ к этим компиляторам в виде API. С его помощью разработчики могут делать много полезных вещей, например очень удобно и просто анализировать, оптимизировать, генерировать код, писать экстеншны и код фиксы для студии, а возможно и собственные DSL. Выйдет она, правда, еще не скоро, аж через одну версию Visual Studio, но пощупать хочется уже сейчас.
Перейдем к решению нашей задачи и прежде всего представим, как бы нам хотелось видеть использование данного расширения языка в действии? Очевидно: мы пишем код, как обычно, в любимой IDE, используем где надо операторы безопасного вызова, жмем Build, во время компиляции написанная нами с помощью Project Roslyn утилита преобразует все это в синтаксически верный C#-код и вуа-ля, все скомпилировано. Спешу вас разочаровать — Roslyn не позволяет вмешиваться в процесс работы текущего компилятора csc.exe, что в принципе довольно объяснимо. Вполне вероятно, если в той самой vNext студии компилятор заменят на его Managed аналог, то такая возможность появится. Но пока ее нет.
В то же время, существует аж два обходных пути:
Project Roslyn предоставляет несколько видов функциональности, однако одна из ключевых — построение, разбор и преобразование абстрактного синтаксического дерева. Именно эту его функциональность мы и будем использовать далее.
Конечно, все написанное ниже лишь пример, страдает от множества пороков и не может использоваться в реальности без существенных доработок, однако показывает, что такие вещи сделать в принципе можно.
Перейдем к реализации. Для того, чтобы написать программу, нам прежде всего надо установить Roslyn SDK, который скачивается по ссылке, также предварительно придется поставить Service Pack 1 для Visual Studio 2010, и Visual Studio 2010 SDK SP1.
После всех этих операций в меню создания новых проектов появится подпункт Roslyn, который включает в себя несколько шаблонов проектов (некоторые из которых могут интегрироваться в IDE). Мы создадим простое консольное приложение.
Для примера будем использовать следующий «исходный код»:
Данный исходный код за исключением операторов безопасного вызова являются не только синтаксически правильным, но и компилируемым, хотя для нашего преобразования это и не обязательно.
Прежде всего, необходимо по файлу с исходным кодом построить абстрактное синтаксическое дерево. Делается это в два счета:
Синтаксическое дерево задается классом SyntaxTree и представляет собой, как ни странно, дерево узлов, наследуемых от базового типа SyntaxNode, каждый из которых представляет некоторое выражение — бинарные выражения, условные выражения, выражения вызова методов, определения свойств и переменных. Естественно, абсолютно любая конструкция C# может быть отображена некоторым экзмепляром класса-наследника SyntaxNode. Кроме того, класс SyntaxTree содержит в себе наборы SyntaxToken, определяющих разбор исходного кода на уровне минимальных синтаксических блоков — ключевых слов, литералов, идентификаторов и пунктуации (фигурные и круглые скобки, запятые, точки с запятыми). Наконец, SyntaxTree в содержит в себе элементы SyntaxTrivia — те, которые по большому счету не важны для понимания кода — пробелы и табуляции, комментарии, директивы препроцессора и.т.д.
Тут следует знать одну небольшую деталь — Roslyn является очень толерантным к синтаксическому разбору файлов. То есть, хотя по-хорошему, ему для разбора надо подавать синтаксически корректный исходный код, на самом деле он абсолютно любой текст пытается некоторым образом преобразовать в некоторое AST. В том числе и наш синтаксически неверный код. Этим фактом мы и воспользуемся. Попробуем построить синтаксическое дерево, и выяснить, каким же образом Roslyn отображает в дереве наш оператор безопасного вызова.
Оказывается все просто: с точки зрения Roslyn выражение test?.TestField является тернарным оператором с условием — «test», выражением «когда верно» — ".TestField", и пустым выражением «когда неверно». Вооружившись этой информацией, будем преобразовывать наше дерево. Тут натыкаемся еще на одну особенность Roslyn — строимое им синтаксическое дерево является неизменяемым, т. е. поправить что-либо прямо в имеющейся структуре не получится. Но не беда. Roslyn предлагает для такой операции использовать класс SyntaxRewriter, который наследует класс SyntaxVisitor, который, как следует из названия, имплментирует небезызвестный паттерн Visitor. Он содержит в себе множество виртуальных методов, обрабатывающих посещение узла каждого конкретного типа (например VisitFieldDeclaration, VisitEnumMemberDeclaration,… всего их порядка 180 штук).
Нам необходимо создать своего наследника класса SyntaxRewriter и переопределить метод VisitConditionalExpression, который вызывается, когда визитор обходит выражение, являющееся тернарным оператором. Далее я приведу целиком код имплементации, тем более, что он невелик, и добавлю лишь некоторые пояснения:
Отмечу, что первая моя реализация пыталась работать только с логической структурой AST, брезгуя работой с текстовым представлением выражений, но сложность ее очень скоро стала превышать все мыслимые пределы. Одних только функций для определения безопасного вызова и его типа было три штуки: для полей и свойств, для вызова методов, для цепочек безопасных вызовов, ибо все это представлялось разными наследниками класса SyntaxNode, и еще множество функций для преобразования различных типов безопасных операторов. Совершенно выдохнувшись, я выбросил первый вариант в мусорку и во второй раз я воспользовался удобными функциями GetText и ParseExpression, которые предоставляет Roslyn и некоторыми грязными хаками на уровне строк :).
Также советую обратить внимание на процесс создания синтаксического узла (в данном случае ConditionalExpression) и приятность использования в этом случае такой фишки C#, как именованные параметры. Ручаюсь, если бы ее не было, в процессе построения синтаксических узлов можно было бы сойти с ума.
Приведем теперь код основной процедуры:
Поясню, что несколько перезаписей дерева необходимо для того, чтобы обработать цепочки вызовов. Конечно это можно было сделать рекурсией, но пожалуй в данном случае это только затуманило бы код. Также обратите внимание на чудесную функцию Format. Она программно делает заданное стилистическое форматирование кода, т.е. добавляет в AST все необходимые SyntaxTrivia.
В результате имеем следующий код:
Итак, первое знакомство с Roslyn прошло успешно, и перспективы его в целом, не обязательно для написания языковых расширений, видятся очень неплохие. Возможно, если есть энтузиасты, этим можно было бы заняться глубже и серьезнее. В C# же есть еще много, чего нам не хватает. :)
P. S. Еще один пример подобного использования Roslyn, который мне значительно помог, приведен здесь.
Пожалуй, C# дает своим адептам и меньше поводов для такой зависти, в сравнении с многими другими, поскольку динамично развивается, добавляя все новые и новые упрощающие жизнь фичи. И все же, нет предела совершенству, причем для каждого из нас — своего.
Сразу отмечу, что приоритетом в данной работе для меня было желание попробовать на зуб Roslyn, а сама идея, которую я дальше опишу, была скорее поводом и тестовым примером для испытаний этой библиотеки. Однако в процессе изучения и реализации я выяснил, что хоть и с некоторыми бубноплясками, но результат действительно можно использовать на практике для реального расширения синтаксиса языка. Как это сделать, кратко опишу в самом конце. А пока приступим.
Безопасные вызовы и монада Maybe
Идея безопасных вызовов заключается в том, чтобы избавиться от надоедающих проверок любых классов на null, которые являясь необходимостью, в то же время значительно засоряют код и ухудшают его читабельность. В то же время, нет никакого желания находиться под постоянной угрозой выпадения NullReferenceException.
Данная проблема решена в функциональных языках программирования с помощью монады Maybe, суть которой заключается в том, что после boxing'а использующийся в конвеерных вычислениях тип может содержать некоторое значение, либо значение Nothing. В случае, если предыдущее вычисление в конвеере дало некоторый результат, то производится следующее вычисление, если же оно вернуло Nothing, то вместо следующего вычисления вновь возвращается Nothing.
В С# созданы все условия для реализации данной монады — вместо Nothing используется null, для структурных типов могут использоваться их Nullable<T> версии. В принципе, идея уже витает в воздухе, и было несколько статей, в которых реализовывалась данная монада в C# с помощью LINQ. Одна из них принадлежит Дмитрию Нестеруку mezastel, есть еще и другая.
Но нельзя не отметить, что при всей заманчивости такого подхода, результирующий код с использованием монады выглядит весьма туманно, из-за необходимости использовать вместо прямых вызовов обертки из лямбда-функций и LINQ. Однако без синтаксических средств языка реализовать ее более элегантно вряд ли представляется возможным.
Достаточно элегантный, как мне показалось, способ реализации данной идеи я обнаружил в спецификации еще пока не созданного языка Kotlin для JDK от ребят из горячо мною любимой компании JetBrains (Null-safety). Как оказалось, такой есть уже в Groovy, возможно и еще где-то.
Итак, что же это за оператор безопасного вызова? Предположим, у нас есть выражение:
string text = SomeObject.ToString();
В случае, если SomeObject является null, мы неминуемо, как уже говорилось, получим NullReferenceException. Чтобы этого избежать, определим в дополнение к оператору прямого вызова '.' еще и оператор безопасного вызова '?.' который выглядит следующим образом:
string text = SomeObject?.ToString();
и представляет собой на самом деле выражение:
string text = SomeObject != null ? SomeObject.ToString() : null;
В случае, если безопасно вызываемый метод или свойство возвращает структурный тип, необходимо чтобы присваиваемая переменная имела тип Nullable.
int? count = SomeList?.Count;
Как и обычные вызовы, такие безопасные вызовы можно использовать цепочками, например:
int? length = SomeObject?.ToString()?.Length;
который преобразуется в выражение:
int? length = SomeObject != null ? SomeObject.ToString() != null ? SomeObject.ToString().Length : null : null;
Здесь скрывается некоторый недостаток предлагаемого мной преобразования, поскольку оно порождает дополнительные вызовы функций. На самом деле желательно было бы преобразовывать его, например, к виду:
var temp = SomeObject;
string text = null;
if (temp != null)
text = temp.ToString();
Однако в ввиду некоторой многословности Roslyn, для того, чтобы примеры не были бы чересчур раздутыми и занудными, я решил сделать преборазование попроще. Впрочем об этом в следующих частях.
Project Roslyn
Как вы может уже слышали, совсем недавно была выпущена CTP версия проекта Roslyn, в рамках которого разработчиками языков C# и VB были полностью переписаны компиляторы языков с использованием managed кода, и открыт доступ к этим компиляторам в виде API. С его помощью разработчики могут делать много полезных вещей, например очень удобно и просто анализировать, оптимизировать, генерировать код, писать экстеншны и код фиксы для студии, а возможно и собственные DSL. Выйдет она, правда, еще не скоро, аж через одну версию Visual Studio, но пощупать хочется уже сейчас.
Перейдем к решению нашей задачи и прежде всего представим, как бы нам хотелось видеть использование данного расширения языка в действии? Очевидно: мы пишем код, как обычно, в любимой IDE, используем где надо операторы безопасного вызова, жмем Build, во время компиляции написанная нами с помощью Project Roslyn утилита преобразует все это в синтаксически верный C#-код и вуа-ля, все скомпилировано. Спешу вас разочаровать — Roslyn не позволяет вмешиваться в процесс работы текущего компилятора csc.exe, что в принципе довольно объяснимо. Вполне вероятно, если в той самой vNext студии компилятор заменят на его Managed аналог, то такая возможность появится. Но пока ее нет.
В то же время, существует аж два обходных пути:
- Можно создать свой собственный компилятор взамен нынешнему csc.exe с использованием все того же Roslyn API, и изменить свою build-систему, заменив csc.exe на свой аналог, включив в него помимо дефолтной компиляции (довольно, кстати, просто программирующейся) свои предварительные преобразования кода.
- Вы можете использовать свою консольную программу в качестве Pre-Build задачи, которая преобразует файлы исходного кода и сохраняет полученные новые исходники в папку Obj. Очень похожим образом осуществляется в данный момент компиляция WPF, когда xaml файлы в фазе pre-build преобразуются в .g.cs файлы.
Project Roslyn предоставляет несколько видов функциональности, однако одна из ключевых — построение, разбор и преобразование абстрактного синтаксического дерева. Именно эту его функциональность мы и будем использовать далее.
Имплементация
Конечно, все написанное ниже лишь пример, страдает от множества пороков и не может использоваться в реальности без существенных доработок, однако показывает, что такие вещи сделать в принципе можно.
Перейдем к реализации. Для того, чтобы написать программу, нам прежде всего надо установить Roslyn SDK, который скачивается по ссылке, также предварительно придется поставить Service Pack 1 для Visual Studio 2010, и Visual Studio 2010 SDK SP1.
После всех этих операций в меню создания новых проектов появится подпункт Roslyn, который включает в себя несколько шаблонов проектов (некоторые из которых могут интегрироваться в IDE). Мы создадим простое консольное приложение.
Для примера будем использовать следующий «исходный код»:
public class Example
{
public const string CODE =
@"using System;
using System.Linq;
using System.Windows;
namespace HelloWorld
{
public class TestClass
{
public string TestField;
public string TestProperty { get; set; }
public string TestMethod() { return null; }
public string TestMethod2(int k, string p) { return null; }
public TestClass ChainTest;
}
public class OtherClass
{
public void Test()
{
TestClass test;
string testStr1;
testStr1 = test?.TestField;
string testStr3 = test?.TestProperty;
string testStr4 = test?.TestMethod();
string testStr5 = test?.TestMethod2(100, testStr3);
var test3 = test?.ChainTest?.TestField;
}
}
}";
}
Данный исходный код за исключением операторов безопасного вызова являются не только синтаксически правильным, но и компилируемым, хотя для нашего преобразования это и не обязательно.
Прежде всего, необходимо по файлу с исходным кодом построить абстрактное синтаксическое дерево. Делается это в два счета:
SyntaxTree tree = SyntaxTree.ParseCompilationUnit(Example.CODE);
SyntaxNode root = tree.Root;
Синтаксическое дерево задается классом SyntaxTree и представляет собой, как ни странно, дерево узлов, наследуемых от базового типа SyntaxNode, каждый из которых представляет некоторое выражение — бинарные выражения, условные выражения, выражения вызова методов, определения свойств и переменных. Естественно, абсолютно любая конструкция C# может быть отображена некоторым экзмепляром класса-наследника SyntaxNode. Кроме того, класс SyntaxTree содержит в себе наборы SyntaxToken, определяющих разбор исходного кода на уровне минимальных синтаксических блоков — ключевых слов, литералов, идентификаторов и пунктуации (фигурные и круглые скобки, запятые, точки с запятыми). Наконец, SyntaxTree в содержит в себе элементы SyntaxTrivia — те, которые по большому счету не важны для понимания кода — пробелы и табуляции, комментарии, директивы препроцессора и.т.д.
Тут следует знать одну небольшую деталь — Roslyn является очень толерантным к синтаксическому разбору файлов. То есть, хотя по-хорошему, ему для разбора надо подавать синтаксически корректный исходный код, на самом деле он абсолютно любой текст пытается некоторым образом преобразовать в некоторое AST. В том числе и наш синтаксически неверный код. Этим фактом мы и воспользуемся. Попробуем построить синтаксическое дерево, и выяснить, каким же образом Roslyn отображает в дереве наш оператор безопасного вызова.
Оказывается все просто: с точки зрения Roslyn выражение test?.TestField является тернарным оператором с условием — «test», выражением «когда верно» — ".TestField", и пустым выражением «когда неверно». Вооружившись этой информацией, будем преобразовывать наше дерево. Тут натыкаемся еще на одну особенность Roslyn — строимое им синтаксическое дерево является неизменяемым, т. е. поправить что-либо прямо в имеющейся структуре не получится. Но не беда. Roslyn предлагает для такой операции использовать класс SyntaxRewriter, который наследует класс SyntaxVisitor, который, как следует из названия, имплментирует небезызвестный паттерн Visitor. Он содержит в себе множество виртуальных методов, обрабатывающих посещение узла каждого конкретного типа (например VisitFieldDeclaration, VisitEnumMemberDeclaration,… всего их порядка 180 штук).
Нам необходимо создать своего наследника класса SyntaxRewriter и переопределить метод VisitConditionalExpression, который вызывается, когда визитор обходит выражение, являющееся тернарным оператором. Далее я приведу целиком код имплементации, тем более, что он невелик, и добавлю лишь некоторые пояснения:
// Находит в синтаксическом дереве операторы безопасного вызова и заменяет их на тернарные операторы
public class SafeCallRewriter : SyntaxRewriter
{
//Был ли в данный проход заменен хотя бы один оператор ?.
public bool IsSafeCallRewrited { get; set; }
protected override SyntaxNode VisitConditionalExpression(ConditionalExpressionSyntax node)
{
if (IsSafeCallExpression(node))
{
//Строим expression для объекта, проверяемого на null
string identTxt = node.Condition.GetText();
ExpressionSyntax ident = Syntax.ParseExpression(identTxt);
//Строим expression для кода, вызываемого при успешной проверка на != null
string exprTxt = node.WhenTrue.GetText();
exprTxt = exprTxt.Substring(1, exprTxt.Length - 1);//убираем точку из записи выражения
exprTxt = identTxt + '.' + exprTxt;
ExpressionSyntax expr = Syntax.ParseExpression(exprTxt);
ExpressionSyntax synt =
Syntax.ConditionalExpression(//тернарный оператор
condition: Syntax.BinaryExpression(//проверяемое условие ident != null
SyntaxKind.NotEqualsExpression,
left: ident, //левый операнд - проверяемый объект
right: Syntax.LiteralExpression(SyntaxKind.NullLiteralExpression)), //литерал null
whenTrue: expr,
whenFalse: Syntax.LiteralExpression(SyntaxKind.NullLiteralExpression));
IsSafeCallRewrited = true;
return synt;
}
return base.VisitConditionalExpression(node);
}
//Является ли тернарный оператор на самом деле оператором безопасного вызова
private bool IsSafeCallExpression(ConditionalExpressionSyntax node)
{
return node.WhenTrue.GetText()[0] == '.';
}
}
Отмечу, что первая моя реализация пыталась работать только с логической структурой AST, брезгуя работой с текстовым представлением выражений, но сложность ее очень скоро стала превышать все мыслимые пределы. Одних только функций для определения безопасного вызова и его типа было три штуки: для полей и свойств, для вызова методов, для цепочек безопасных вызовов, ибо все это представлялось разными наследниками класса SyntaxNode, и еще множество функций для преобразования различных типов безопасных операторов. Совершенно выдохнувшись, я выбросил первый вариант в мусорку и во второй раз я воспользовался удобными функциями GetText и ParseExpression, которые предоставляет Roslyn и некоторыми грязными хаками на уровне строк :).
Также советую обратить внимание на процесс создания синтаксического узла (в данном случае ConditionalExpression) и приятность использования в этом случае такой фишки C#, как именованные параметры. Ручаюсь, если бы ее не было, в процессе построения синтаксических узлов можно было бы сойти с ума.
Приведем теперь код основной процедуры:
static void Main(string[] args)
{
//Строим синтаксическое дерево
SyntaxTree tree = SyntaxTree.ParseCompilationUnit(Example.CODE);
SyntaxNode root = tree.Root;
SafeCallRewriter rewriter = new SafeCallRewriter();
do
{
rewriter.IsSafeCallRewrited = false;
//Обходим дерево, производя заданные операции в различных типах узлов и переписывая дерево
root = rewriter.Visit(root);
} while (rewriter.IsSafeCallRewrited);//за предыдущий проход был найден и преобразован хоть 1 maybe-оператор
root = root.Format();//программный Ctrl+K, Ctrl+D
Console.WriteLine(root.ToString());
}
Поясню, что несколько перезаписей дерева необходимо для того, чтобы обработать цепочки вызовов. Конечно это можно было сделать рекурсией, но пожалуй в данном случае это только затуманило бы код. Также обратите внимание на чудесную функцию Format. Она программно делает заданное стилистическое форматирование кода, т.е. добавляет в AST все необходимые SyntaxTrivia.
В результате имеем следующий код:
using System;
using System.Linq;
using System.Windows;
namespace HelloWorld
{
public class TestClass
{
public string TestField;
public string TestProperty
{
get;
set;
}
public string TestMethod()
{
return null;
}
public string TestMethod2(int k, string p)
{
return null;
}
public TestClass ChainTest;
}
public class OtherClass
{
public void Test()
{
TestClass test;
string testStr1;
testStr1 = test != null ? test.TestField : null;
string testStr3 = test != null ? test.TestProperty : null;
string testStr4 = test != null ? test.TestMethod() : null;
string testStr5 = test != null ? test.TestMethod2(100, testStr3) : null;
var test3 = test != null ? test.ChainTest != null ? test.ChainTest.TestField : null : null;
}
}
}
Итак, первое знакомство с Roslyn прошло успешно, и перспективы его в целом, не обязательно для написания языковых расширений, видятся очень неплохие. Возможно, если есть энтузиасты, этим можно было бы заняться глубже и серьезнее. В C# же есть еще много, чего нам не хватает. :)
P. S. Еще один пример подобного использования Roslyn, который мне значительно помог, приведен здесь.