Сообщений 7    Оценка 15        Оценить  
Система Orphus

Язык Nemerle

Часть 6

Автор: Чистяков Владислав Юрьевич
Источник: RSDN Magazine #2-2011
Опубликовано: 20.02.2012
Исправлено: 10.12.2016
Версия текста: 1.0
Синтаксис Nemerle
Базовый язык
Расширяемость
Единица компиляции (CompilationUnit)
Пространства имен (NamespaceDeclaration)
Тело пространства имен (NamespaceBody)
Директива «using»
Декларации типов определяемых пользователем (TypeDeclaration)
Псевдоним типа (TypeAliasDeclaration)
Классы, структуры, интерфейсы и модули (ClassDeclaration, StructDeclaration. InterfaceDeclaration)
Перечисления (EnumDeclaration)
Вариантный тип (VariantDeclaration)
Вхождение вариантного типа (VariantOptionDeclaration)
Делегаты (DelegateDeclaration)
Модификаторы
Атрибуты
Типы данных поддерживаемые Nemerle
Описание типа (Type)
Примеры описаний типов и их конструкторов
Одномерный массив
Многомерный массив
Функциональный тип
Кортеж
Обычные типы
Nullable-типы
Члены типа (TypeMemberDeclaration)
FieldDeclaration
MethodDeclaration
Заголовок функции (FunctionHeader)
PropertyDeclaration
Событие (EventDeclaration)
IndexerDeclaration
ConstructorDeclaration
Parameters
Операторы
Ссылки

Синтаксис Nemerle

Перед тем, как начать изучать синтаксис Nemerle, нужно уяснить несколько важных вещей.

Первое, что нужно понять: Nemerle – язык с расширяемым синтаксисом, причем расширяемым динамически. Любая директива «using», встретившаяся в исходном файле Nemerle, может подключить синтаксическое расширение.

Из первого замечания следует, что при наличии в некоторой области видимости синтаксического расширения любое выражение или даже конструкция верхнего уровня (такая как свойство или метод) могут быть разобраны компилятором Nemerle не так, как предполагает базовый язык.

Последнее, что нужно понять - что имеется некоторый базовый синтаксис, даже если не использовать ни одного макроса.

Базовый язык

Фактически Nemerle можно разделить на три отдельные части:

  1. Базовый язык, разбираемый компилятором Nemerle в условиях, когда не подключено ни одного синтаксического макроса.
  2. Nemerle – язык, включающий макросы из пространства имен Nemerle.Core (входящие в стандартную библиотеку макросов).
  3. Nemerle, расширенный макросами из других пространств имен стандартной макро-библиотеки или сторонних макро-библиотек.

Использовать базовый язык практически невозможно. В нем отсутствуют даже такие вещи, как приоритеты операторов. Это может быть полезно только для целей обучения (как в первой части данной работы), или с целью создания собственной замены стандартной библиотеки, например, чтобы изменить синтаксис и/или семантику стандартных операторов и выражений.

Несмотря на то, что базовый язык практически невозможно использовать, в разделах, посвященных синтаксису Nemerle будет описан именно он.

Представление о синтаксисе Nemerle можно получить, сложив синтаксис базового языка и синтаксис стандартных макросов. Остальные же синтаксические расширения нужно рассматривать как подключаемые DSL.

Расширяемость

Несмотря на то, что Nemerle – язык с расширяемым синтаксисом, невозможно изменить синтаксис базового языка или стандартных макросов (если, конечно, не изменить код компилятора или стандартной библиотеки макросов).

Однако можно:

  1. Расширять синтаксис.
  2. Интерпретировать имеющийся синтаксис иным образом.

В обоих случаях, чтобы это сделать, понадобятся макросы.

С расширением синтаксиса все должно быть довольно понятно. Введение нового синтаксического макроса и открытие пространства имен, в котором он объявлен, заставит компилятор учитывать новый синтаксис.

Но что же понимается под интерпретацией имеющегося синтаксиса?

Дело в том, что компилятор Nemerle разбирает более абстрактный синтаксис, нежели допускает синтаксис Nemerle. Например, во всех местах, где производится разбор ссылок на тип, поначалу разбирается не синтаксис ссылки на тип, а синтаксис выражения Nemerle. В дальнейшем, когда компилятор начнет типизировать полученное AST (абстрактное синтаксическое дерево получаемое в результате парсинга), он проверит, соответствует ли разобранное выражение ссылке на тип и выдаст соответствующее сообщение об ошибке, если это не так.

Зачем же нужно такая двойная проверка? Дело в том, что вторая проверка происходит не сразу же по окончании парсинга. В промежутке времени между парсингом и типизацией выражения в дело может вмешаться макрос. Он может самостоятельно разобрать полученное выражение и переписать код так, чтобы он стал корректным кодом Nemerle. Например, именно так поступают макросы из пространства имен Nemerle.ComputationExpressions, которые реализуют возможность, аналогичную Computation Expressions из F#.

Такой подход называется реинтерпретацией синтаксиса. Он используется наряду с изменением синтаксиса для реализации разного рода встраиваемых DSL.

ПРЕДУПРЕЖДЕНИЕ

В описании синтаксиса базового языка, который будет приведен ниже, будет приводиться конечный синтаксис Nemerle (т.е. синтаксис, который допускает весь компилятор, а не только парсер). Однако если некоторая синтаксическая конструкция допускает реинтерпретацию синтаксиса, об этом будет упомянуто отдельно.

Единица компиляции (CompilationUnit)

Единица компиляции (CompilationUnit) определяет структуру компилируемого файла. Структура файла Nemerle несколько проще, нежели структура файла C#, которая послужила прототипом. Простота определяется рекурсивностью дизайна.

CompilationUnit = NamespaceBody;

В Nemerle единица компиляции полностью аналогична телу пространства имен. Это позволяет:

  1. Упростить синтаксис языка, и, следовательно, упростить его изучение.
  2. Использовать глобальные атрибуты (атрибуты уровня сборки) как непосредственно в единице компиляции, так и внутри пространства имен. Для обычных атрибутов это ничего не дает. Но для макроатрибутов уровня сборки это позволяет задать пространство имен, в рамках которого может действовать макрос.

Ниже приведен пример макро-атрибута добавляющий типы. Для добавления типа этот макрос использует метод Define из типа GlobalEnv, ссылка на который получается из свойства typer.Env. Это позволяет определить тип в текущем пространстве имен.

        using Nemerle;
using Nemerle.Collections;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Compiler.Typedtree;
using Nemerle.Utility;

namespace MacroLibrary
{
  [MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Assembly)]
  macro CreateClass(name : PExpr)
  {
    def typer = Macros.ImplicitCTX();
      
    def toName(expr : PExpr) : Name 
    { 
      | <[ $(name : name) ]> => name 
      | _ => Message.FatalError(expr.Location, "Expected simple name.") 
    }
      
    def newTypeBuilder = typer.Env.Define(
<[ decl: publicclass $(toName(name) : name) { } ]>);

    Message.Hint($"The $(newTypeBuilder.FullName) type is defined.");
    newTypeBuilder.Compile();
  }
}

Вот так может выглядеть использование данного макроса:

        using MacroLibrary;

[assembly: CreateClass(TestClass1)]

namespace NamespaceA
{
  [assembly: CreateClass(TestClass2)]

  namespace NamespaceB
  {
    [assembly: CreateClass(TestClass3)]
  }
}

При этом в окно Output будет выведено:

hint: The TestClass1 type is defined.
hint: The NamespaceA.TestClass2 type is defined.
hint: The NamespaceA.NamespaceB.TestClass3 type is defined.

что говорит о том, что типы были созданы в тех пространствах имен, в которых макрос был применен.

Пространства имен (NamespaceDeclaration)

Синтаксис объявления пространства имен в Nemerle ничем не отличается от синтаксиса, принятого в C#:

NamespaceDeclaration = "namespace" QualifiedIdentifier 
                       "{" NamespaceBody "}"";"?;

Пример объявления пространства имен:

        namespace NamespaceA
{
  // Тело NamespaceAnamespace NamespaceB
  {
    // Тело NamespaceA.NamespaceBnamespace NamespaceC
    {
      // Тело NamespaceA.NamespaceB.NamespaceC
    }
  }
  
  namespace NamespaceC
  {
    // Тело NamespaceA.NamespaceC
  }
}

namespace NamespaceC
{
  // Тело NamespaceC
}

namespace NamespaceA.NamespaceB.NamespaceC
{
  // Тело NamespaceA.NamespaceB.NamespaceC
}

Тело пространства имен (NamespaceBody)

Тело пространства имен состоит из необязательной группы директив «using», необязательной группы объявлений глобальных атрибутов и необязательного списка членов пространства имен.

NamespaceBody = (UsingDirective 
                 | GlobalAttributeSection 
                 | NamespaceDeclaration
                 | TypeDeclaration
                 )*;

Пример:

        using System;

namespace NamespaceA
{
  using System.Text.RegularExpressions;
  
  namespace NamespaceB
  {
    publicclass Class1 // class: NamespaceA.NamespaceB.Class1
    {
      private WordRegex : Regex = Regex(@"\w+");
    }
  }
  
  publicclass Class1 // class: NamespaceA.Class1
  {
    // System.Text.RegularExpressions.Regexprivate WordRegex : Regex = Regex(@"\w+");
  }
}

Директива «using»

Директива «using» имеет две разновидности:

  1. Позволяет открыть пространство имен или тип.
  2. Позволяет задать псевдоним для типа или пространства имен.
UsingDirective          = UsingAliasDirective | UsingNamespaceDirective;
UsingAliasDirective     = "using" Identifier "=" Type ";"
UsingNamespaceDirective = "using" NamespaceName ";"

К типам открытых пространств имен можно обращаться по неквалифицированному имени.

        namespace NamespaceA
{
  class ClassA { }
}

using NamespaceA;

module Program
{
  Main() : void
  {
    def a = ClassA(); // тоже самое что NamespaceA.ClassA()
  }
}

Если в открытом пространстве имен были объявлены синтаксические макросы, парсер будет использовать вводимые ими синтаксические расширения в области видимости, на которую распространяется действие данной директивы using.

        using Nemerle.Text;

def res =
  regexpmatch("10va") // синтаксический макрос regexp match
  {
    | @"(?<a : int>\d+)(va)?" => a
    | _ => -1
  };

assert(res == 10);

Если директивой using был открыт тип, то ко всем его статическим членам можно обращаться по неквалифицированному имени.

        using System.Console; // модуль System.Console

WriteLine("Test");    // System.Console.WriteLine

Вторая разновидность директивы using позволяет задать псевдоним для типа или пространства имен. Это позволяет избежать неоднозначности, если в разных пространствах имен имеются типы с одинаковыми именами:

        using SCG = System.Collections.Generic;
using SC = System.Collections;


def q = SCG.Queue(); // q имеет тип System.Collections.Generic.Queue[int]
q.Enqueue(1);

def q = SC.Queue();  // q имеет тип System.Collections.Queue
q.Enqueue(1);

Декларации типов определяемых пользователем (TypeDeclaration)

Список типов, поддерживаемых Nemerle, описан в разделе «Типы данных поддерживаемые Nemerle». В числе поддерживаемых Nemerle типов есть как предопределенные (например, кортежи или функциональный тип), так и определяемые пользователем (пользовательские) типы.

В Nemerle поддерживаются следующие пользовательские типы:

Кроме того, в Nemerle имеется возможность задать псевдоним для типа (type alias).

TypeDeclaration = ClassDeclaration     | StructDeclaration
                | InterfaceDeclaration | EnumDeclaration
                | DelegateDeclaration  | VariantDeclaration
                | TypeAliasDeclaration;

Псевдоним типа (TypeAliasDeclaration)

TypeAliasDeclaration = "type" Identifier TypeParameters? "=" Type;

Псевдоним типа позволяет дать типу другое имя (обычно более краткое) и/или поместить тип в другое пространство имен.

Ссылки на псевдонимы типов заменяются ссылками на реальные типы. Таким образом, в сгенерированном коде не остается ссылок на псевдонимы.

Ссылка на псевдоним типа эквивалентна ссылке на исходный тип (тип, для которого создавался синоним). Поэтому в коде можно смешивать использование синонимов и исходных типов.

ПРИМЕЧАНИЕ

Обратите внимание на то, что синонимы не вводят новый тип. Это всего лишь синоним для имеющегося типа.

Примеры:

        using SCG = System.Collections.Generic;

namespace Nemerle.Collections
{
  publictype Seq[T] = SCG.IEnumerable[T];
}

using Nemerle.Collections;

module Program
{
  Test() : Seq[int]
  {
    Enumerable.Range(0, int.MaxValue)
  }
}

В данном примере введен краткий псевдоним Seq для типа IEnumerable. Псевдоним введен в пространстве имен Nemerle.Collections, таким образом, тип Seq будет доступен, когда открыто пространство имен Nemerle.Collections, или при указании квалифицированного имени: Nemerle.Collections.Seq.

ПРИМЕЧАНИЕ

Синоним Seq уже имеется в стандартной библиотеке. Вы можете использовать его в своем коде без предварительного объявления.

Другие примеры:

        internal
        type MapStrToList[T] = SCG.Dictionary[string, SCG.List[T]];
type DynStr = System.Text.StringBuilder;

В отличие от директивы using, синонимы, будучи однажды объявленными, могут использоваться во всех файлах проекта, а если они объявлены с директивной «public», то и из других проектов, к которым подключена сборка, в которой объявлен псевдоним.

Классы, структуры, интерфейсы и модули (ClassDeclaration, StructDeclaration. InterfaceDeclaration)

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

ClassDeclaration =
  Attribits? Modifiers? "partial"? "class" Name TypeParameters?
    SuperTypes? 
    ConstraintsClause*
  "{"
      TypeMemberDeclaration*
  "}";

Name              = Identifier;

Различия между классами, модулями, интерфейсами и структурами:

  1. Типы различаются ключевыми словами: class, module, interface или struct.
  2. У них различаются допустимые модификаторы. О том, какие модификаторы и когда применимы, можно прочесть в разделе «Модификаторы».
  3. Модуль не может реализовывать интерфейсы, так как все методы модуля автоматически превращаются в статические. Однако модуль может иметь базовый класс. По сути, модуль аналогичен статическому классу в C#.
  4. У интерфейса не может быть базового класса (только базовые интерфейсы).
  5. Интерфейс не может быть частичным (для интерфейсов неприменим модификатор «partial»).
  6. У интерфейса отличаются также описания членов. У них не может быть модификаторов доступа и тел.

Базовые типы (SuperTypes)

SuperTypes = ":" TypeNames;
TypeNames  = TypeName ("," TypeName)*;

Базовые типы или супер-типы описывают список типов, подтипом (sub type) которых является данный тип.

У класса может быть один базовый класс. Любой класс имеет базовый класс. Если базовым классом является object (System.Object), его можно не указывать.

Класс может реализовывать ноль или более интерфейсов. Если класс унаследован от базового класса, то реализуемые интерфейсы должны идти за базовым классом.

Только наследование класса позволяет унаследовать реализацию (членов). Реализация интерфейсов позволяет унаследовать интерфейс, т.е. использовать тип в контексте, где ожидается интерфейс.

Интерфейс может быть унаследован только от других интерфейсов. При этом базовых интерфейсов может быть произвольное количество (ноль или более).

В качестве базовых типов для структур могут выступать только интерфейсы. Структуры не могут наследовать реализацию.

Модули могут иметь базовый класс, но не могут реализовывать интерфейсы, так как в модулях допустимы только статические члены, а интерфейсы, напротив, допускают только нестатические члены.

Пример:

          using System;
using System.Console;

publicclass Person
{
  // Конструктор, создающий и инициализирующий объект.publicthis(name : string, lastName : string, age : DateTime)
  {
    Name     = name;
    LastName = lastName;
    Age      = age;
  }
  
  public Name     : string   { get; }
  public LastName : string   { get; }
  public Age      : DateTime { get; }
}

[Record] // Генерирует конструктор, инициализирующий все поля объекта.publicclass Employee : Person
{
  public Position   : string { get; }
  public Department : string { get; }
}

def employee1 = Employee(
  name="John", lastName="Carmack", 
  age=DateTime(1970, 08, 20),
  position="Co-founder",
  department="Game development");
def employee2 = Employee("John", "Romero",  DateTime(1967, 10, 28), 
                         "Co-founder", "Game design");
def employees = [employee1, employee2];
def result = employees.Filter(e => e.Age.Year >= 1970);
    
foreach (employee in result)
  WriteLine($"$(employee.Name), $(employee.LastName) $(employee.Age)");

Вывод:

John, Carmack 20.08.1970 00:00:00

Параметры типов (TypeParameters)

TypeParameters = "[" TypeParamName ("," TypeParamName)* "]";
TypeParamName  = Identifier;

Параметры типов описывают имена типов, которые будет заменены реальными типами во время конструирования экземпляра типа, для которого они описаны, или при вызове обобщенного метода (метода, для которого описаны параметры типов).

Тип или метод, для которого описаны параметры типов, называется обобщенным.

Обобщенные типы и методы похожи на шаблонны C++, но это все же не одно и то же. Шаблоны C++ – это сущность времени компиляции, а обобщенные типы и методы – это runtime-сущность. Поэтому, с одной стороны, обобщенные типы и методы могут быть скомпилированы в IL и помещены в библиотеку (сборку), но с другой, на них налагается большее количество ограничений, нежели на шаблоны.

На параметры типов накладываются следующие ограничения:

ПРЕДУПРЕЖДЕНИЕ

Обратите внимание на то, что в отличие от C#, в Nemerle значение, тип которого задан параметром типа, можно сравнивать с null только если для этого параметром типа задано ограничение «class».

Более подробно об обобщенных типах и методах можно прочесть в любом описании generics в .NET или C#.

Пример, реализующий «динамический массив» – коллекцию, позволяющую хранить произвольное количество элементов произвольного типа (упрощенная версия класса):

          using Nemerle.Imperative; // требуется для использования breakusing System;
using System.Collections.Generic;
using System.Console;

publicclass MyList[T] : IList[T]
{
  public Count : int { get; privateset; }
  mutable _items : array[T] = array(16);
  
  private EnsureCapacity(capacity : int) : void
  {
    when (capacity > _items.Length)
    {
      def data = array(Math.Max(_items.Length * 2, capacity));
      Array.Copy(_items, data, _items.Length);
    }
  }
  
  #region System.Collections.Generic.IList[T]  Members
  
  public Add(value : T) : void { Insert(Count, value); }
  
  public IndexOf(item : T) : int { Array.IndexOf(_items, item, 0, Count) }
  
  public Insert(index : int, item : T) : void
  {
    EnsureCapacity(Count + 1);
    
    if (index > Count)
      throw IndexOutOfRangeException("index");
    elsewhen (index < Count)
      Array.Copy(_items, index, _items, index + 1, Count - index);
    
    _items[index] = item;
    Count++;
  }
  
  public Item[index : int] : T // индексатор (для доступа по индексу)
  {
    get { _items[index] }
    set { _items[index] = value; }
  }
  
  public RemoveAt(index : int) : void
  {
    Array.Copy(_items, index + 1, _items, index, Count - index);
    Count--;
  }
  
  #endregionpublic Clear() : void{ Count = 0; }
  
  public Contains(item : T) : bool { IndexOf(item) >= 0 }
  
  public CopyTo(arr : array[T], arrayIndex : int) : void
  {
    Array.Copy(_items, 0, arr, arrayIndex, Count);
  }
  
  public IsReadOnly : bool { get { false } }
  
  public Remove(item : T) : bool
  {
    def index = IndexOf(item);
    
    when (index > 0)
      RemoveAt(index);
      
    index >= 0
  }
  
  #region System.Collections.Generic.IEnumerable[T]  Members
  
  public GetEnumerator() : IEnumerator[T]
  {
    foreach (item in _items with i)
      if (i == Count)
        break;
      elseyield item;
  }
  
  #endregion
}

publicmodule Program
{
  Main() : void
  {
    def myList = MyList();
    
    myList.Add(1);
    myList.Add(2);
    myList.Add(3);
    myList.Insert(1, 42);
    myList.Insert(0, 0);

    WriteLine($"xs = '..$myList' Count=$(myList.Count)");
    
    def x = myList[2]; // использование индексатора
    WriteLine($"xs[2] = $x");
    
    _ = myList.Remove(2);
    myList.RemoveAt(2);
    
    WriteLine($"xs = '..$myList' Count=$(myList.Count)");
  }
}

Выводит:

xs = '0, 1, 42, 2, 3' Count=5
xs[2] = 42
xs = '0, 1, 3' Count=3

ConstraintsClause

ConstraintsClause = "where" TypeParamName ":" Constraints;
Constraints       = Constraint | (", " Constraint)*;
Constraint        = "class" | "struct" | "new" | "enum" | TypeName;
SuperType         = TypeName;

На параметры типов могут накладываться ограничения (constraints). Ограничения позволяют ограничить список типов, которые могут быть подставлены в качестве параметра типов. С другой стороны, они позволяют обращаться к членам экземпляра типа, заданного при помощи параметра типов.

Nemerle поддерживает следующие типы ограничений (под «типами» в этом перечислении понимаются типы, подставляемые в качестве параметра типа):

В списке ограничений может быть задан ноль или один базовый тип (супер-тип, SuperType), не являющийся интерфейсом (например, структура или класс), и ноль или более базовых типов, являющихся интерфейсами. В качестве супер-типа могут выступать другие параметры типа (как имеющие, так и не имеющие ограничений).

В отличие от C#, допускается задавать один интерфейс несколько раз с разными параметрами типов, если в результате конкретной подстановки не может получиться один и тот же тип. Например, в качестве ограничения можно задать: IComparable[string], IComparable[object], но нельзя IComparable[string], IComparable[T] (где T – это один из параметров типа), так как в качестве T может быть подставлен string.

Тип, подставляемый в качестве параметра типов, должен удовлетворять всем ограничениям одновременно. При этом, если заданы ограничения «SuperType», то подставляемый тип должен быть наследником (подтипом) всех указанных в ограничениях супер-типов. Например, если в списке ограничений есть интерфейсы ICollection[T] и IDictionary[TKey, TValue], то в такой параметр типа можно будет подставить только тип, реализующий оба этих интерфейса (например, класс Dictionary[TKey, TValue]).

Перечисления (EnumDeclaration)

EnumDeclaration =
  Attribits? Modifiers? "enum" Name EnumBase?
  "{"
      EnumMember*
  "}";

EnumBase     = ":" IntegralType;
IntegralType = "sbyte" | "byte"  | "short" | "ushort" | "int" | "uint" 
             | "long"  | "ulong" | "decimal";
EnumMember   = Attribits? "|" Name ("=" ConstantExpression)?

ConstantExpression = PExpr; // поддерживаются только константные выражения
Name               = Identifier;

Если для элемента перечисления не задано значение, то оно вычисляется как значение элемента, идущего выше, плюс один. Для самого первого элемента значением по умолчанию является 0.

Если для элемента задано значение (после знака «=»), то используется именно оно.

При использовании перечисления для описания набора битовых флагов можно применять атрибут System.Flags. Это позволит получить более качественную поддержку таких перечислений.

ПРЕДУПРЕЖДЕНИЕ

Возможно, что в будущих версиях Nemerle будут запрещены битовые операции с перечислениями, не помеченными этим атрибутом.

Пример 1:

        using System.Console;

publicenum X : long
{
  | A
  | B
  | C
}

def pring(x)
{
  def type           = x.GetType().Name;
  def underlyingType = x.GetType().GetEnumUnderlyingType();
      
  WriteLine($"$type.$x = $(x :> long) ($underlyingType)");
}
    
pring(X.A);
pring(X.B);
pring(X.C);

Выводит:

X.A = 0 (System.Int64)
X.B = 1 (System.Int64)
X.C = 2 (System.Int64)

Пример 2:

        using System.Console;

enum X : long
{
  | A
  | B = 40 + 2
  | C
}

def pring(x)
{
  def type           = x.GetType().Name;
  def underlyingType = x.GetType().GetEnumUnderlyingType();
      
  WriteLine($"$type.$x = $(x :> long) ($underlyingType)");
}
    
pring(X.A);
pring(X.B);
pring(X.C);

Выводит:

X.A = 0 (System.Int64)
X.B = 42 (System.Int64)
X.C = 43 (System.Int64)

Пример 3:

        using System.Console;

[Flags]
publicenum X : long
{
  | A = 0x1
  | B = 0x2
  | C = 0x4
}

def pring(x)
{
  def type           = x.GetType().Name;
  def underlyingType = x.GetType().GetEnumUnderlyingType();
      
  WriteLine($"$type.$x = $(x :> long) ($underlyingType)");
}
    
pring(X.A);
pring(X.B | X.C);
pring(X.C);

Выводит:

X.A = 1 (System.Int64)
X.B, C = 6 (System.Int64
X.C = 4 (System.Int64)

Вариантный тип (VariantDeclaration)

VariantDeclaration =
  Attribits? Modifiers? "variant" Name TypeParameters?
    SuperTypes? 
    Constraints?
  "{"
      (TypeMemberDeclaration | VariantOptionDeclaration)*
  "}";

Вариантный тип Nemerle является реализацией Алгебраических Типов Данных (АлгТД). Вариантный тип имеет следующие особенности:

  1. Может иметь так называемые вхождения вариантного типа (variant options). Вхождения вариантного типа (далее – вхождения) являются подтипами вариантного типа (наследниками, в терминах ООП). Соответственно, сам вариантный тип является базовым типом (супертипом) для всех своих вхождений.
  2. Каждое вхождение определяет хотя бы один конструктор, описывающий вхождение. Таким образом, вариантный тип описывается набором конструкторов, с помощью которых можно создать вариантный тип (а также распознать его с помощью сопоставления с образцом).
  3. Список вхождений является фиксированным. Его нельзя расширять с помощью наследования или другими средствами. Таким образом, вхождения являются запечатанными (sealed) в терминах ООП.
  4. Сам вариантный тип является абстрактным (в терминах ООП), т.е. невозможно создать его экземпляр напрямую (можно только создать его вхождение).

Вариантный тип сочетает в себе черты, унаследованные как от реализаций АлгТД в функциональных языках вроде ML и OCaml, так и черты, унаследованные от объектно-ориентированных языков.

От ООП вариантный тип позаимствовал возможность наследования от базового типа (которым должен быть класс), возможность реализовывать интерфейсы, а также вводить и переопределять виртуальные члены. Таким образом, вариантный тип в Nemerle является не чистой реализацией АлгТД, а некоторым гибридным решением, сочетающим возможности АлгТД с ОО-возможностями.

Вариантный тип является закрытым, в том смысле, что его подтипами могут быть только его же вхождения, которые должны быть перечислены в объявлении вариантного типа. Вхождения так же не могут иметь наследников. Однако сам вариантный тип может быть наследником некоторого незапечатанного класса. Если вариантный тип не унаследован от некоторого класса явно, по умолчанию считается, что он является наследником типа object.

Вариантный тип является ссылочным типом данных.

Примеры вариантных типов можно увидеть в следующем разделе.

Вхождение вариантного типа (VariantOptionDeclaration)

VariantOptionDeclaration = Attribits? "|" Name VariantOptionBody?;
VariantOptionBody        = "{" TypeMemberDeclaration* "}" ";"?

Вхождение вариантного типа (далее – вхождения) описывает подтип вариантного типа.

Для вхождения формируется конструктор, имеющий по одному параметру для каждого поля или авто-свойства (включая private-члены). Если требуется исключить поле или авто-свойство из конструктора, его следует пометить макро-атрибутом RecordIgnore. Для формирования конструктора компилятор добавляет макро-атрибут Record к вариантному типу.

По умолчанию (если модификатор доступа не задан явно) поля вхождения являются публично доступными (public). Если во вхождении имеются непубличные поля, то они входят в состав формируемого автоматически конструктора, но не используются при сопоставлении с образцом, так как сопоставление с образцом может оперировать только публично доступными значениями.

В метаинформацию вхождения записывается информация о соответствии полей/авто-свойств и параметров формируемого автоматически конструктора. Это позволяет разбирать значения вариантных типов с помощью паттерна "конструктор" в операции сопоставления с образцом.

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

Кроме автоматически генерируемого конструктора, вариантный тип может содержать и определяемые пользователем конструкторы. Однако такие конструкторы можно использовать только для конструирования объекта, но нельзя использовать в качестве образца при сопоставлении с образцом.

Автоматически генерируемый конструктор может быть заменен конструктором, определяемым пользователем. Для этого перед ключевым словом «this» нужно добавить ключевое слово «new». Код конструктора при этом может быть произвольным, но подразумевается, что он должен поместить значения параметров конструктора в значения соответствующих полей. В противном случае сопоставление с образцом для таких вхождений может работать некорректно.

Можно рассматривать перечисление (enum) как вырожденный случай вариантного типа. Однако между перечислениями и вариантными типами есть существенная разница:

Пример:

        variant Color
{
  | Red
  | Yellow
  | Green
  | Rgb    { red : byte; green : byte; blue : byte; }
  | Alpha  { color : Color; alpha : byte; }
  
  public ToRgb() : byte * byte * byte
  {
    match (this)
    {
      | Red          => (255b, 0b,   0b)
      | Green        => (0b,   255b, 0b)
      | Yellow       => (255b, 255b, 0b)
      | Rgb(r, g, b) => (r, g, b)
      | Alpha(x, _)  => x.ToRgb()
    }
  }
}

def print(color : Color) : void
{
  | Red    | Rgb(255,   0, 0) => WriteLine("Red");
  | Yellow | Rgb(255, 255, 0) => WriteLine("Yellow");
  | Green  | Rgb(0,   255, 0) => WriteLine("Green");
  | Rgb(r, g, b)              => WriteLine($"Rgb($r, $g, $b)")
  | Alpha(x, _)               => print(x)
}
    
print(Color.Yellow());
print(Color.Rgb(255, 255, 0));
print(Color.Alpha(Color.Green(), 128));

def colors = [Color.Yellow(), Color.Green(), Color.Rgb(255, 0, 0)];
WriteLine(colors.Map(_.ToRgb()));

Результат:

Yellow
Yellow
Green
[(255, 255, 0), (0, 255, 0), (255, 0, 0)]

Делегаты (DelegateDeclaration)

DelegateDeclaration  = Attribits? Modifiers? "delegate" MethodHeader ";";

Для использования функций как первоклассных значений (передачи ссылок на функции и методы) в Nemerle имеется специальный функциональный тип. Однако Nemerle поддерживает также специальный вид типов – «Делегаты» (Delegates).

Делегаты добавлены в Nemerle для совместимости с другими .NET-языками, для многих из которых делегаты являются единственным способом манипулировать ссылками на функции.

Основные отличия делегатов от встроенного в Nemerle функционального типа:

Nemerle автоматически приводит функциональный тип к делегату при условии совпадения сигнатуры. Обратное преобразование невозможно, но у делегатов обычно есть метод Invoke. Этот метод можно использовать в случае, когда имеется делегат, а нужно получить функциональный объект:

        using System;
using System.Console;

def test(f : int -> int) : int
{
  f(5)
}

// Функциональный объект приводится к делегату.def d : Func[int, int] = x => x * x;
// В функцию, требующую функциональный объект, передается // ссылка на метод Invoke делегата.
WriteLine(test(d.Invoke));

Результат:

25

Пример объявления и использования делегата:

        using System.Console;

publicdelegateConvert(x : int) : string;

def convert(value : int) : string
{
  "'" + value.ToString() + "'"
}

def print(value : int, convert : Convert) : void
{
  WriteLine(convert(value));
}
    
print(42, Convert(convert)); // явное создание делегата
print(42, convert);          // неявное создание делегата
print(42, _.ToString("X"));

Результат:

'42'
'42'
2A

Модификаторы

Modifiers = AccessModifiers | "mutable" | "volatile" | "static" | "new" 
          | "abstract" | "sealed"  | "override" | "virtual" 
          | "partial" | "extern";
AccessModifiers = "public" | "private" | "protected" | "internal";

Применимость модификаторов зависит от того, к описанию чего они применяются, и от того, является ли сущность типом верхнего уровня или членом типа (вложенные типы рассматриваются как член типа).

Не допускается многократное применение модификаторов на одном объявлении.

Модификаторы доступа: public, private, protected и internal определяют видимость типа или члена извне. Для типов верхнего уровня (объявленных в пространствах имен, а не вложенных в другие типы) допустимыми модификаторами доступа являются public и internal. Если у типа верхнего уровня не указаны модификаторы доступа, то по умолчанию считается, что тип имеет internal-доступ.

Для типов верхнего уровня модификаторы доступа имеют следующее значение:

Для членов типов (включая вложенные типы) доступны следующие значения:

Для членов реальные права доступа вычисляются из модификаторов типа и модификаторов, примененных ко всем внешним типам (типам, в которые он вложен). Например, если некоторый тип описан с модификатором доступа public, но хотя бы один из внешних типов имеет модификатор private, то данный тип не будет виден извне.

Модификаторы abstract, sealed и static позволяют описать, является ли тип абстрактным, запечатанным или статическим.

Абстрактный тип может содержать абстрактные члены (члены без тел). Экземпляры таких типов нельзя создать. От таких типов можно только унаследовать другие типы.

Запечатанные типы – это типы, от которых нельзя унаследовать другие типы (противоположность абстрактным).

Статический тип – это тип, все члены которого – статические. В Nemerle для обозначения таких типов есть специальный тип – модуль.

abstract и sealed не могут использоваться совместно.

Модификатор new может применяться для вложенных типов чтобы предотвратить появления предупреждения о том, что тип скрывать другой тип с тем же именем вложенный в базовый тип.

Атрибуты

Attribits           = AttributeSection+;
AttributeSection    = "[" (AttributeTarget ":")? AttributeList ","? "]";
AttributeTarget     = "field" | "event" | "method" | "param" | "property"
                    | "return" | "type" | "assembly" | "module";
AttributeList       = Attribute ("," Attribute)*;
Attribute           = AttributeName AttributeArguments?;
AttributeArguments  = "(" AttributeArgument (","AttributeArgument)* ","? ")";
AttributeArgument   = PositionalArgument | NamedArgument;
PositionalArgument  = PExpr;
NamedArgument       = Identifier = PExpr;
AttributeName       = TypeName; 

Атрибут в Nemerle может быть как атрибутом в понимании .Net, так и макро-атрибутом. На стадии парсинга между ними не делается различий. И описание атрибутов, и описание макро-атрибутов хранятся в одном виде – в виде AST.

Если атрибут является атрибутом .Net, то AttributeName должен ссылаться на не обобщенный тип напрямую или косвенно унаследованный от класса System.Attribute. Если атрибут имеет суффикс Attribute в своем имени, то этот суффикс можно опустить при ссылке на атрибут.

Если атрибут является макро-атрибутом, то AttributeName должен полностью совпадать с именем макроса.

При разборе аргументов атрибутов парсер всегда разбирает выражение Nemerle (PExpr). Если разбираемый атрибут впоследствии окажется макро-атрибутом, то выражение передается макросу в неизменном виде. Если же он окажется обычным атрибутом, то будет проведено распознавание именованных параметров. Именованные параметры должны идти после позиционных.

Типы данных поддерживаемые Nemerle

В Nemerle поддерживаются следующие типы данных:

Примитивные типы делятся на строковый тип (string), числовые типы и void. Более подробно о встроенных типах данных написано в разделе «Встроенные типы данных» первой части.

Все типы также делятся на ссылочные типы (reference types) и типы значений (value types).

К ссылочным типам относятся: классы, интерфейсы, вариантные типы, массивы, функциональный тип, кортежи (хранящие более трех элементов), делегаты, строки, запакованные типы.

К типам значений относятся: структуры, перечисления, кортежи (хранящие менее четырех элементов), числовые примитивные типы, Nullable-типы.

Экземпляры ссылочных типов всегда размещаются в управляемой куче и передаются по ссылке.

ПРИМЕЧАНИЕ

В отличие от C#, в Nemerle не поддерживается тип «указатель». Таким образом, все ссылочные объекты в Nemerle являются управляемыми (типобезопасными). Для взаимодействия с неуправляемыми объектами можно применять тип IntPtr, специальное API и атрибут DllImportAttribute.

Типы значений передаются по значению, за исключением случая использования ref- или out-параметров функций. Экземпляры типов значений могут размещаться на стеке или внутри других объектов.

Запакованные типы (boxed type) – это типы-значения, к которым применена процедура запаковки. При этом значение переносится в кучу, и возвращается ссылка на область кучи, в которой размещено значение. В запакованном виде можно обращаться к интерфейсам, реализованным в типе значения или к методам, описанным в типе object (например, ToString). Однако, пока значение остается запакованным, невозможно обратиться к нему или к членам. Чтобы сделать это, необходимо произвести процедуру распаковки типа.

В Nemerle запаковка производится путем приведения типа к типу object или к интерфейсу, реализуемому типом значения.

ПРИМЕЧАНИЕ

Обратите внимание на то, что при использовании ограничений на параметрах типов, при обращении к интерфейсам, реализованным типом, подставленным в качестве параметра типа, запаковка не производится. Это позволяет при работе с обобщенными типами и методами генерировать эффективный код, когда в качестве параметров подставляются типы значений.

Описание типа (Type)

Описание тип – это имя типа, используемое, когда нужно сослаться на него из других мест приложения (например, при описании типа параметра функции).

В Nemerle введен специализированный синтаксис для описания функциональных типов, массивов, кортежей и Nullable-типов. Остальные типы описываются универсальным образом.

Type = TypeName | ArrayType | TupleType | FunctionType | NullableType;

// Специализированный синтаксис для описания типов
FunctionType  = Type "->" Type;                       // Функциональный тип
TupleType     = Type ("*" Type)+;                     // Кортежи
ArrayType     = "array""[" Types? "]";               // Одномерный массив
ArrayType     = "array" . "[" Rank "] "[" Types? "]"; // Многомерный массив
Rank          = ['0'..'9']; // размерность многомерного массива
Types         = Type ("," Type)*; // список типов, разделенных запятой
NullableType  = Type "?"; // Type должен быть значимым (value-type)// Общий синтаксис описания типов для остальных типов
TypeName         = QualifiedIdentifier
TypeName         = Identifier TypeArguments?;
                 | TypeName : 285 "." Identifier TypeArgumentList?
                 | TypeName : 283 "?";
TypeArguments    = "."?"[" TypeArgument (","TypeArgument)* "]";
TypeArgument     = TypeName | "_";

К TypeName относятся пользовательские типы (структуры, классы, вариантные типы и псевдонимы типов), объявленные в проекте или подключенных к проекту библиотеках, и встроенные типы (string, int, double, void и т.п.).

Встроенные типы, за исключением void, являются псевдонимами, описанными в стандартной библиотеке (Nemerle.dll), в пространстве имен Nemerle.Core (это пространство имен открыто по умолчанию). Подробнее о встроенных типах данных написано в разделе «Встроенные типы данных» первой части.

При описании ссылки в качестве аргументов типа вместо указания конкретного типа можно использовать символ заместитель «_» (wildcard). Это приводит к инициализации параметра типа переменной типа. Переменная типа может принимать любое значение типа. Для получения открытого обобщенного типа нужно задать все параметры типов как «_».

Ссылка на тип, у которой не задан хотя бы один аргумент типа, имеет значение «_» являтся нефиксированной. Нефиксированная ссылка на тип не может использоваться для описания типов конструкций верхнего уровня (типов и их членов). Однако такая ссылка на тип может применяться внутри метода. Например, если необходимо указать, что типом возвращаемого значения является список, но не хочется задавать конкретный тип для элементов списка, то можно поступить следующим образом:

      def function() : list[_]
{
  ...
}

При этом тип элемента будет выведен из тела функции и/или из ее использования.

Примеры описаний типов и их конструкторов

Одномерный массив

Одномерный массив целых (int):

        array[int]

Примеры использования:

Неизменяемая переменная типа массив целых длиной 42 элемента, инициализированный нулями (значениями, принятыми по умолчанию для int):

          def elements : array[int] = array(42);

Изменяемая переменная типа массив целых, инициализированная значением «null»:

          mutable elements : array[int];
// Инициализация
elements = array(3);
// Изменение элемента с индексом 1 (нижняя граница 0)
elements[1] = 42;

Неизменяемая переменная типа массив целых, инициализированная массивом из трех элементов содержащего значения 1, 2 и 3:

          def elements = array[1, 2, 3];

Многомерный массив

Двумерный массив строк (string):

        array[2, string]

Примеры использования:

Неизменяемая переменная типа двумерный массив целых размером 42 * 42 элемента, инициализированный нулями (значениями, принятыми по умолчанию, для int):

          def elements : array[2, int] = array(2, 42);

Изменяемая переменная типа трехмерный массив целых, инициализированная значением «null»:

          mutable elements : array[3, int];
// Инициализация значениями
elements = array.[3][[[1, 2, 3], [1, 2, 3], [1, 2, 3]]];
// Изменение элемента
elements[0, 1, 2] = 42;

Неизменяемая переменная типа массив целых, инициализированная массивом из трех элементов, содержащим значения 1, 2 и 3:

          def elements = array[1, 2, 3];

Функциональный тип

        // Функция без параметров не возращающая значения
        void -> void// Функция без параметров возращающая значение типа intvoid -> int// Функция с параметром типа string возращающая значение типа intstring -> int// Функция с двумя параметрами типа string и int соответственно// возращающая значение типа intstring * int -> int// Функция с двумя параметрами типа string и int соответственно// возращающая кортеж типа double * bool (т.е. два значения)string * int -> double * bool

Пример использования:

          def doWork(predicate : DateTime -> bool) : void
{
  ...
  when (predicate(DateTime.Now))
    DoStaff();
  ...
}

Кортеж

        // Кортеж состоящий из двух элементов типа string и int
        string * int// Кортеж состоящий из трех элементов типа int, double и boolint * double * bool

Использование:

          def doWork() : int * bool
{
  ...
  if (ok)
    (42, true)
  else
    (-1, false)
}

def (result, ok) = doWork();

when (ok)
  DoStaff(result);

Кортежи и функции в Nemerle имеют много общего. По сути, параметры функций, если их число превышает один, описываются (и рассматриваются) как кортеж (что отчетливо видно при описании типа функции).

Nemerle поддерживает автоматическое преобразование из кортежа в параметры (при условии совместимости типов). Так что следующий пример корректен:

          def foo() : int * bool { ... }
def bar(int * bool) : void { ... }

bar(foo());

def result = foo();
bar(result);

def (_, ok) as result = foo();

when (ok)
  bar(result);

Обычные типы

System.Collections.Generic.List[int]
List[int]
SCG.List[int]

Использование:

          using System.Collections.Generic;
using SC = System.Collections;

def foo() : Queue[int]
{
  def queue = Queue(); // System.Collections.Generic.Queue[int]
  queue.Enqueue(42);   // эта строка позволяет вывести тип queue
  queue
}

def queue = Queue.[long](); // System.Collections.Generic.Queue[long]
queue.Enqueue(42);
def top = queue.Peek();
assert(top == 42);    

def queue = SC.Queue(); // System.Collections.Queue
queue.Enqueue(42);
def top : int = queue.Peek() :> int;
assert(top == 42);

Обратите внимание, что при указании параметров типов у конструкторов обобщенных типов необходимо указывать «.» для устранения неоднозначности с индексаторами. Это не очень красивое решение. Однако в Nemerle почти всегда можно просто не указывать параметры типов вовсе. Как показано в примере выше, Nemerle сам выводит значения параметров типов из использования.

Nullable-типы

        def print[T](value : T?)
  where T: struct
{
  if (value.HasValue)
    WriteLine(value);
  else
    WriteLine("No value!");
}

mutable x : int? = null;
print(x);
x = 42;
print(x);

mutable y : DateTime? = DateTime.Now;
print(y);
y = null;
print(y);

Использование:

No value!
42
26.12.2011 18:25:21
No value!

Члены типа (TypeMemberDeclaration)

Члены типа состоят из членов, описанных непосредственно в типе, а также членов, описанных в базовых типах (супертипах).

TypeMemberDeclaration = FieldDeclaration
                      | MethodDeclaration
                      | PropertyDeclaration
                      | EventDeclaration
                      | IndexerDeclaration
                      | ConstructorDeclaration
                      | TypeDeclaration;

На члены типов налагаются следующие ограничения:

Унаследованные члены типа могут быть перекрыты членами, объявленными в подтипе. При этом унаследованные члены скрываются.

FieldDeclaration

Поле – это член типа, представляющий переменную, ассоциированную с объектом данного типа или с самим типом (если это статическое поле). Объявление поля (FieldDeclaration) вводит одно поле.

FieldDeclaration = 
  Attribits? FieldModifiers? "mutable"? Name VariableInitializer? ";";

FieldModifiers = 
VariableInitializer = "=" PExpr;

Поле может быть статическим или экземплярным. Статические поля помечаются модификатором static или объявляются внутри модуля (где все члены автоматически помечаются модификатором static).

У поля может быть инициализатор – выражение, задающее начальное значение. Код инициализатора поля копируется в конструкторы таким образом, чтобы быть однократно выполненным при вызове любого конструктора. Если один конструктор вызывает другой, то код инициализации поля помещается только в вызываемый конструктор (что обеспечивает его однократный вызов).

Поле может быть изменяемым или неизменяемым. Значение неизменяемого поля можно задать в одном из конструкторов типа или в инициализаторе поля. Неизменяемость поля не означат неизменяемости объектов, на которые ссылается поле. Изменяемость объекта определяется реализацией его типа.

Изменяемое поле может быть изменено с помощью оператора присвоения – «=», макросов (например, «++») или путем передачи в ref- или out-параметр.

Изменяемое поле может быть помечено модификатором volatile. Некоторые техники оптимизации могут изменить порядок инструкций, осуществляющих доступ к полю. При работе в многопоточном окружении в условиях отсутствия синхронизации это может привести к неожиданным и непредсказуемым результатам. Для полей, помеченных модификатором volatile, такое переупорядочивание ограничено. Модификатор volatile неприменим к полям, тип которых «структура».

MethodDeclaration

Метод – это функция-член типа, выполняющая над ним вычисления или действия.

MethodDeclaration = 
  Attribits? Modifiers? SyntaxExtension? FunctionHeader Implements?
  MethodBody;

Implements = "implements" Implement ("," Implement)
Implement  = TypeName "." Identifier;
MethodBody = Block ";"? | ";";

Методы могут быть:

Метод может реализовывать один или более методов интерфейсов, являющихся супертипом типа, в котором объявлен данный метод. Реализация метода интерфейса означает, что если тип, в котором объявлен метод, будет приведен к интерфейсу, и будет произведен вызов метода интерфейса, то управление получит метод, который реализует метод интерфейса.

Реализация метода интерфейса может быть неявной или явной.

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

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

Метод, реализующий метод интерфейса, не должен быть статическим или внешним, но он может быть виртуальным, абстрактным или запечатанным.

Метод может вводить синтаксическое расширение (SyntaxExtension). При этом фактически синтаксическое расширение, введенное на уровне метода, может не формировать AST метода, а делать все, что ему угодно. Например, добавить реализацию поля, двух свойств и трех методов.

Объявление метода вводит новую область видимости (scope), в которой видны параметры типов и параметры.

Имя метода должно отличаться от имен членов, объявленных в том же типе, но не являющихся методом.

Если имя метода совпадает с именами других методов, то эти методы должны отличаться по числу и/или типам формальных параметров.

Декларация нескольких методов, имеющих одно имя, но различающихся по числу и/или типам параметров, называется перегрузкой. Перегрузка методов допускается только на основании аргументов. Запрещается перегрузка по типу возвращаемого значения.

Примеры объявления и использования методов можно увидеть в разделе «Классы, структуры, интерфейсы и модули (ClassDeclaration, StructDeclaration. InterfaceDeclaration)».

Заголовок функции (FunctionHeader)

Заголовок функции является частью декларации метода или локальной функции. Он состоит из обязательной части – имени и списка параметров, заключенных в круглые скобки, и необязательной: параметров типов (TypeParameters), возвращаемого значения и ограничений (ConstraintsClause).

FunctionHeader = 
  Name TypeParameters? "(" Parameters? ")" (":" Type)? ConstraintsClause*;

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

PropertyDeclaration

Свойство – это член, предоставляющий доступ к состоянию объекта или типа. Например, к длине массива или имени служащего. Свойство можно рассматривать как средство абстракции состояния объекта. В коде обращение к свойству выглядит как обращение к полю. Однако, в отличие от полей, свойство не хранит состояние самостоятельно, а предоставляет интерфейс (в виде эксессоров – методов, вызываемых при попытке узнать или изменить значение свойства).

PropertyDeclaration = 
  Attribits? Modifiers? Name ":" Type "{" PropertyBody "}";

Getter = Attribits? Modifiers? "get" MethodBody;
Setter = Attribits? Modifiers? "set" MethodBody;
PropertyBody = FieldDeclaration? Getter Setter?
             | FieldDeclaration? Setter Getter?;

Для каждого эксессора генерируется метод, имя которого формируется с помощью конкатенации префикса «get_» (для Getter-а), «set_» (для Setter-а) и имени свойства. Например, для свойства Size, имеющего Getter и Setter, будут сформированы два метода get_Size и set_Size. Эти методы не нужны для обычного доступа к свойству, но могут использоваться, если необходимо использовать свойство как ссылку на функцию.

Один из эксессоров свойства может иметь модификаторы доступа, отличающиеся от модификаторов доступа свойства. При этом разрешается понижать уровень доступа эксессора, но не повышать его. Например, если свойство имеет публичный доступ, можно изменить модификаторы доступа Setter-а на private. Это позволит изменять свойство в членах объекта, но не извне. Однако нельзя задать модификатор public для одного из эксессоров, если само свойство имеет иной уровень доступа. Нельзя также задавать модификаторы доступа сразу обоим эксессорам. Вместо этого можно изменить уровень доступа у самого свойства.

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

Кроме обычных свойств, в Nemerle есть так называемые автосвойства (autoproperties). Они удобны, когда необходимо описать декларацию свойства, которое не производит никаких вычислений в своих эксессорах, а просто возвращает или изменяет значение некоторого поля.

Различаются два вида автосвойств:

get-автосвойства – это автосвойства, имеющие только Getter. Поведение таких автосвойств похоже на поведение неизменяемых полей. Их можно проинициализировать (изменить) только в конструкторах.

Обычные автосвойства. У них должны быть объявлены как Getter, так и Setter.

Примеры объявления и использования свойств можно увидеть в разделе «Классы, структуры, интерфейсы и модули (ClassDeclaration, StructDeclaration. InterfaceDeclaration)».

Событие (EventDeclaration)

Событие – это член, предназначенный для реализации оповещения об изменении внутреннего состояния объекта/класса или о внешних явлениях. Клиент может подключать к событию обработчики (подходящие по сигнатуре функции), которые будут вызваны в случае возникновения события.

EventDeclaration = 
  Attribits? Modifiers? "event" Name ":" Type EventBodyBlock?;

EventBodyBlock = "{" EventBody "}";
EventBody      = EventGetter | EventSetter;
EventGetter    = "add"    MethodBody ("remove" MethodBody)?;
EventSetter    = "remove" MethodBody ("add"    MethodBody)?;

Типом события должен быть делегат (см. «Делегаты (DelegateDeclaration)»), уровень доступа такой же, как у события или выше.

Событие может иметь эксессоры событий. Если эксессоры не указаны, то компилятор реализует их логику автоматически.

Для абстрактного свойства не может быть объявлен EventBodyBlock.

IndexerDeclaration

Индексатор позволяет сделать определяемый пользователем тип похожим на массив – использовать оператор индексации (x[i]) для доступа к внутренним значениям. Это удобно при реализации коллекций, хранящих набор элементов, к которым осуществляется индексированный доступ (списки, словари, и т.п.).

PropertyDeclaration = 
  Attribits? Modifiers? Name "[" Parameters "]"":" Type "{" PropertyBody "}";

Если индексатор имеет имя «Item», формируется индексатор по умолчанию. Такой индексатор позволяет применять оператор индексации непосредственно к экземпляру типа. В ином случае добавляется именованное свойство, к которому можно применять оператор индексации.

Индексатор является разновидностью свойства. Поэтому к нему применимо все, что относится к свойствам.

Пример:

        using System.Console;

 [Record]
partialpublicclass B
{
  protectedinternalclass C {}
  
  public NamedIndexer[index : int] : string
  {
    get { "'" + Item[index] + "'" }
  }
  
  public Item[index : int] : string
  {
    _data : array[string] = array(42);

    get { _data[index] }
    set { _data[index] = value; }
  }
}

def b = B(array["0", "1", "2", "3", "4", "5", "6"]);
b[6] = "six";

WriteLine(b[5]);
WriteLine(b[6]);
WriteLine();
WriteLine(b.NamedIndexer[5]);
WriteLine(b.NamedIndexer[6]);

Выводит:

5
six

'5'
'six'

ConstructorDeclaration

ConstructorDeclaration = 
  Attribits? Modifiers? Name "(" Parameters? ")"
  MethodBody;

Parameters

Parameters      = FormalParameter (", " FormalParameter)* ", "?;
FormalParameter = ParameterPrefix? NameOr_ ":" TypePrefix? Type ("=" DefaultValue)?;
ParameterPrefix = Attribits? SyntaxExtensions? ("this" | "params")?;
TypePrefix      = "ref" | "out";
NameOr_         = Name | "_";
DefaultValue    = PExpr;

Модификатор «this» может примениться к первому параметру метода. Метод, к первому параметру которого применен модификатор this, является методом-расширением. Метод-расширение – это статический метод, который может быть вызван как экземплярный метод (через точку).

Модификатор «params» может быть применен к последнему параметру метода. Параметр, к которому применен модификатор «params» называется массивом параметров. Методы, в которых описан массив параметров, могут принимать переменное число аргументов. Тип дополнительных аргументов (т.е. не входящих в список фиксированных параметров) должен совпадать с типом элемента в массиве параметров.

Параметры могут быть ссылочными (с префиксом «ref» перед типом параметра) и возвращаемыми (с префиксом «out» перед типом параметра).

Значение ссылочного параметра передается по ссылке (управляемому указателю). Это означает, что значением такого параметра может выступать только имя изменяемой переменной. Значение такой переменной может быть изменено в процессе вызова метода.

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

Ссылочные и возвращаемые параметры, а также массив параметров, не могут иметь значения по умолчанию (DefaultValue). Для других параметров может быть задано значение по умолчанию.

Значением по умолчанию может быть константное выражение (выражение, которое может быть свернуто в константу/литерал во время компиляции).

В объявлениях параметров могут использоваться синтаксические расширения (SyntaxExtension). Синтаксическое расширение – это макрос, вводящий новый синтаксис (в данном случае уровня параметра). Для создания синтаксического расширения, которое может применяться при объявлении параметра, нужно создать синтаксический макрос уровня параметра.

Операторы

В Nemerle описание операторов аналогично описанию статических методов типа. Единственное различие заключается в именах. Именем оператора является набор операторных символов, перед которым идет знак «@» (преобразующий операторные символы в корректный идентификатор).

Ссылки


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