Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
Iceсream Screen Recorder – это программа для захвата видео с аудио и микрофоном и создания скриншотов с помощью ряда доп...
SlimDrivers — бесплатная программа для обнаружения присутствующих на компьютере драйверов, определение их версий, поиска...
Многофункциональный, кроссплатформенный текстовый редактор с функциями подсветки синтаксиса, авто-подстановкой выражений...
Программа позволяет быстро обнаруживать на раннем этапе возможные отказы в работе сети или веб-сайта. PRTG Network Monit...
Программа для создания презентаций и интерактивных обучающих видеоуроков. Camtasia Studio может осуществлять захват изоб...

Новый и более совершенный C# 6.0

Текущий рейтинг: 3.33 (проголосовало 3)
 Посетителей: 1160 | Просмотров: 1342 (сегодня 0)  Шрифт: - +

Хотя C# 6.0 еще не закончен, он достиг той стадии, когда языковые средства близки к финальным. По сравнению с CTP3-версией C# 6.0 в предстоящем выпуске Visual Studio под кодовым наименованием «14», о которой я рассказывал в своей статье «A C# 6.0 Language Preview» (msdn.microsoft.com/magazine/dn683793.aspx), в язык внесен целый ряд изменений и усовершенствований.

В C# 6.0 внесен ряд изменений и усовершенствований.

В этой статье я рассмотрю новые языковые средства и поясню, какие изменения были внесены в средства, о которых шла речь в предыдущей статье. Я также буду вести блог (itl.tc/csharp6) с полным описанием изменений в каждом языковом средстве C# 6.0. Многие из примеров в данной статье взяты из следующего издания моей книги «Essential C# 6.0» (Addison-Wesley Professional).

Оператор проверки на null

Даже разработчики-новички под .NET скорее всего знакомы с NullReferenceException. Это исключение, которое почти всегда указывает на ошибку в коде, поскольку разработчик не предусмотрел исчерпывающую проверку на null до вызова какого-либо члена пустого (null) объекта. Возьмем, к примеру:

public static string Truncate(string value, int length)
{
  string result = value;
  if (value != null) // Пропускаем пустую строку для пояснения
  {
    result = value.Substring(0, Math.Min(value.Length, length));
  }
  return result;
}

Если бы этой проверки на null не было, метод вызвал бы NullReferenceException. Хотя это просто, необходимость в проверке строкового параметра на null довольно утомительна. А нередко такой подход неприемлем из-за частоты этой проверки. В C# 6.0 включен новый оператор проверки на null (null-conditional operator), который помогает более лаконично писать такие проверки:

public static string Truncate(string value, int length)
{          
  return value?.Substring(0, Math.Min(value.Length, length));
}
[TestMethod]
public void Truncate_WithNull_ReturnsNull()
{
  Assert.AreEqual<string>(null, Truncate(null, 42));
}

Как демонстрирует метод Truncate_WithNull_ReturnsNull, если значение объекта действительно null, то оператор проверки на null вернет null. Это вызывает встречный вопрос: а что будет, когда этот оператор появляется в цепочке вызовов, как в следующем примере:

public static string AdjustWidth(string value, int length)
{
  return value?.Substring(0, Math.Min(value.Length, length)).PadRight(length);
}
[TestMethod]
public void AdjustWidth_GivenInigoMontoya42_ReturnsInigoMontoyaExtended()
{
  Assert.AreEqual<int>(42, AdjustWidth("Inigo Montoya", 42).Length);
}

Хотя Substring вызывается через оператор проверки на null и пустой value?.Substring вроде бы должен вернуть null, язык делает то, что вы хотели бы. Он сокращает вызов до PadRight и сразу же возвращает null, предотвращая ошибку программирования, которая иначе привела бы к NullReferenceException. Это концепция, известная как распространение null (null-propagation).

Оператор проверки на null проверяет условие на null перед вызовом целевого метода и любого дополнительного метода в цепочке вызовов. Потенциально это могло бы дать неожиданный результат, как в выражении text?.Length.GetType.

Если оператор проверки на null возвращает null, когда цель вызова равна null, то каким будет конечный тип данных при вызове члена, возвращающего значимый тип (с учетом того, что значимый тип не может быть равен null)? Например, тип данных, возвращаемый value?.Length, просто не может быть int. Правильный ответ, конечно же, — int?. Фактически попытка присвоить результат просто int приведет к ошибке компиляции:

int length = text?.Length; // Ошибка компиляции: нельзя неявно
// преобразовать тип 'int?' в 'int'

Оператор проверки на null имеет две синтаксические формы. Первая — это знак вопроса перед оператором «точка» (?.). Во второй используется знак вопроса в сочетании с оператором index. Например, в случае набора вместо явной проверки на null перед индексацией в наборе вы можете воспользоваться оператором проверки на null:

public static IEnumerable<T> GetValueTypeItems<T>(
  IList<T> collection, params int[] indexes)
  where T : struct
{
  foreach (int index in indexes)
  {
    T? item = collection?[index];
    if (item != null) yield return (T)item;
  }
}

В этом примере используется индексная форма оператора проверки на null (?[…]), при которой индексация в наборе происходит, только если набор не равен null. При этой форме оператора проверки на null, выражение T? item = collection?[index] по поведению эквивалентно:

T? item = (collection != null) ? collection[index] : null.

Оператор проверки на null может лишь считывать элементы. Присвоить элемент не получится. Что это означает применительно к null-набору?

Разработчики на C# на протяжении последних четырех выпусков интересовались, будет ли улучшен вызов делегатов. Наконец-то это должно произойти.

Обратите внимание на неявную неопределенность при использовании ?[…] применительно к ссылочному типу. Так как ссылочные типы могут быть null, то null-результат от оператора ?[…] не позволяет понять, что было равно null — набор или сам элемент.

Одно из особенно полезных применений оператора проверки на null устраняет специфическую особенность C#, существовавшую со времен C# 1.0: проверку на null перед вызовом делегата. Рассмотрим код на C# 2.0, показанный на рис. 1.

Рис. 1. Проверка на null до вызова делегата

class Theremostat
{
  event EventHandler<float> OnTemperatureChanged;
  private int _Temperature;
  public int Temperature
  {
    get
    {
      return _Temperature;
    }
    set
    {
      // Если есть подписчики, уведомляем их
      // об изменениях температуры
      EventHandler<float> localOnChanged =
        OnTemperatureChanged;
      if (localOnChanged != null)
      {
        _Temperature = value;
        // Вызываем подписчики
        localOnChanged(this, value);
      }
    }
  }
}

При использовании оператора проверки на null вся реализация set сводится к простому выражению:

OnTemperatureChanged?.Invoke(this, value)

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

Разработчики на C# на протяжении последних четырех выпусков интересовались, будет ли улучшен вызов делегатов. Наконец-то это должно произойти. Одно это изменит то, как вы вызываете делегаты.

Другой распространенный шаблон, где оператор проверки на null мог бы стать широко применяемым, — его комбинация с оператором слияния (coalesce operator). Вместо проверки на null в linesOfCode до вызова Length вы можете написать алгоритм подсчета элементов так:

List<string> linesOfCode = ParseSourceCodeFile("Program.cs");
return linesOfCode?.Count ?? 0;

В этом случае любой пустой набор (без элементов) и null-набор нормализуются так, чтобы вернуть одинаковый счетчик. Если вкратце, то оператор проверки на null будет:

  • возвращать null, если операнд — null;
  • сокращать дополнительные вызовы в цепочке вызовов, если операнд — null;
  • возвращать тип, допускающий null-значение (System.Nullable<T>), если целевой член возвращает значимый тип;
  • поддерживать вызов делегата безопасным в многопоточной среде способом;
  • доступен и как оператор члена (?.), и как оператор индекса (?[…]).

Инициализаторы автоматически реализуемых свойств

Любой .NET-разработчик, который когда-либо должным образом реализовал struct, несомненно сетовал на то, сколько синтаксических изысков требуется для того, чтобы сделать тип неизменяемым (как это предполагаю стандарты .NET). Предметом спора является тот факт, что свойство только для чтения должно:

  • иметь поддерживающее поле (backing field), определенное только для чтения;
  • инициализировать это поле из конструктора;
  • быть явно реализуемым свойством (а не автоматически реализуемым);
  • иметь явную реализацию аксессора get, которая возвращает поддерживающее поле.

И все это лишь для того, чтобы правильно реализовать неизменяемое свойство. Затем это повторяется для всех свойств в типе. Поэтому корректная работа требует значительно больше усилий, чем «хрупкий» подход. Здесь на помощь придет C# 6.0, в котором появилось новое средство — инициализаторы автоматически реализуемых свойств (auto-property initializers) (в CTP3 также включена поддержка выражений инициализации). Инициализатор автоматически реализуемого свойства обеспечивает задание свойств непосредственно в их объявлении. В случае свойств только для чтения он берет на себя весь церемониал, необходимый для того, чтобы сделать его неизменяемым. Возьмем, к примеру, класс FingerPrint:

public class FingerPrint
{
  public DateTime TimeStamp { get; } = DateTime.UtcNow;
  public string User { get; } =
    System.Security.Principal.WindowsPrincipal.Current.Identity.Name;
  public string Process { get; } =
    System.Diagnostics.Process.GetCurrentProcess().ProcessName;
}

Как видно из кода, инициализаторы свойства обеспечивают задание свойству начального значения в самом объявлении. Свойство может быть только для чтения (только аксессор get) или для чтения и записи (оба аксессора — set и get). Если оно только для чтения, нижележащее поддерживающее поле автоматически объявляется с модификатором «только для чтения». Это гарантирует его неизменяемость после инициализации.

Корректная работа требует значительно больше усилий, чем «хрупкий» подход.

Инициализатором может быть любое выражение. Например, использую условный оператор, вы можете задать инициализирующее значение по умолчанию:

public string Config { get; } = string.IsNullOrWhiteSpace(
  string connectionString =
    (string)Properties.Settings.Default.Context?["connectionString"])?
  connectionString : "<none>";

В этом примере обратите внимание на использование выражения объявления (см. itl.tc/?p=4040), как мы обсуждали в прошлой статье. Если вам нужно нечто большее, чем выражение, вы могли бы преобразовать инициализацию в статический метод и вызывать его.

Выражения nameof

Еще одно пополнение в CTP3-версии — поддержка выражений nameof. Бывают ситуации, когда в коде нужно использовать «магические строки». Это обычные строки в C#, которые сопоставляются с программными элементами в вашем коде. Например, когда возникает исключение ArgumentNullException, вы должны использовать строку для имени соответствующего параметра, который оказался недопустимым. К сожалению, магические строки не проверяются при компиляции, а при любом изменении программного элемента (скажем, при переименовании параметра) не происходит автоматического обновления магической строки, что приводит к рассогласованию, никогда не отлавливаемому компилятором.

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

Чтобы устранить эту особенность, C# 6.0 предоставляет доступ к имени программного элемента, будь то имя класса, имя метода, имя параметра или имя конкретного атрибута (возможно, при использовании механизма отражения). Например, в коде на рис. 2 с помощью выражения nameof извлекается имя параметра.

Рис. 2. Получение имени параметра с помощью выражения nameof

void ThrowArgumentNullExceptionUsingNameOf(string param1)
{
  throw new ArgumentNullException(nameof(param1));
}
[TestMethod]
public void NameOf_UsingNameofExpressionInArgumentNullException()
{
  try
  {
    ThrowArgumentNullExceptionUsingNameOf("data");
    Assert.Fail("This code should not be reached");
  }
  catch (ArgumentNullException exception)
  {
    Assert.AreEqual<string>("param1", exception.ParamName);
}

Как демонстрирует тестовый метод, свойство ParamName в ArgumentNullException имеет значение param1 — это значение задано через выражение nameof(param1) в методе. Выражение nameof не ограничено параметрами. Оно годится для получения имени любого программного элемента, как показано на рис. 3.

Рис. 3. Получение имен других программных элементов

namespace CSharp6.Tests
{
  [TestClass]
  public class NameofTests
  {
    [TestMethod]
    public void Nameof_ExtractsName()
    {
      Assert.AreEqual<string>("NameofTests", nameof(NameofTests));
      Assert.AreEqual<string>("TestMethodAttribute",
        nameof(TestMethodAttribute));
      Assert.AreEqual<string>("TestMethodAttribute",
        nameof(
         Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute));
      Assert.AreEqual<string>("Nameof_ExtractsName",
        string.Format("{0}", nameof(Nameof_ExtractsName)));
      Assert.AreEqual<string>("Nameof_ExtractsName",
        string.Format("{0}", nameof(
        CSharp6.Tests.NameofTests.Nameof_ExtractsName)));
    }
  }
}

Выражение nameof извлекает только конечный идентификатор, даже если вы используете более явные имена с точками (dotted names). Кроме того, в случае атрибутов суффикс «Attribute» не подставляется. Это необходимо для правильной компиляции и дает отличную возможность разгрести запутанный код.

Основные конструкторы

Инициализаторы автоматически реализуемых свойств особенно полезны в сочетании с основными конструкторами (primary constructors). Основные конструктора позволяют сократить формальности в распространенных шаблонах объектов. Это средство значительно улучшено по сравнению с майским выпуском. Обновления включают следующее.

  1. Реализовать тело основного конструктора не обязательно. Но такая реализация позволяет выполнять, в частности, проверку и инициализацию параметров основного конструктора, что ранее не поддерживалось.
  2. Исключение параметров-полей для объявления полей через параметры основного конструктора. (Отказ от развития этого средства в том виде, в каком оно было определено, было правильным решением, поскольку это больше не заставляет применять специфические соглашения по именованию такими способами, которые ранее были противоречивы для C#.)
  3. Поддержка функций и свойств с телами в выражениях (expression bodied) (об этом мы поговорим позже).

С распространением веб-сервисов, многоуровневых приложений, сервисов данных, Web API, JSON и схожих технологий одной из популярных форм класса стал объект передачи данных (data transfer object, DTO). DTO, как правило, не обладает широким функционалом, а сконцентрирован на простоте сохранения данных. Эта концентрация на простоте делает очень интересными основные конструкторы. Рассмотрим, например, неизменяемую структуру данных Pair:

struct Pair<T>(T first, T second)
{
  public T First { get; } = first;
  public T Second { get; } = second;
  // Оператор равенства...
}

Определение конструктора — Pair(string first, string second) — объединяется с объявлением класса. Это указывает, что параметрами конструктора являются first и second (каждый из которых имеет тип T). На эти параметры также ссылаются инициализаторы свойств и присваивают их своим соответствующим свойствам. Видя простоту этого определения класса, его поддержку неизменяемости и обязательного конструктора (инициализатора всех свойств/полей), вы понимаете, насколько сильно это помогает в корректном написании кода. Это значительно улучшает распространенный шаблон, который ранее требовал излишней «многословности».

Тела основных конструкторов указывают поведение этих конструкторов. Это помогает реализовать эквивалентную возможность в основных конструкторах так же, как это делается в конструкторах в целом. Например, следующий шаг в повышении надежности структуры данных Pair<T> мог бы заключаться в проверке свойства. Такая проверка гарантировала бы, что null-значение для Pair.First было бы недопустимым. В CTP3 теперь включено тело основного конструктора (primary constructor body) — тело конструктора без объявления, как показано на рис. 4.

Рис. 4. Реализация тела основного конструктора

struct Pair<T>(T first, T second)
{
  {
    if (first == null) throw new ArgumentNullException("first");
    First = first; // ВНИМАНИЕ: в CTP3 это не работает
  }     
  public T First { get; }; // ВНИМАНИЕ: в CTP3
// дает ошибку компиляции
  public T Second { get; } = second;
  public int CompareTo(T first, T second)
  {
    return first.CompareTo(First) + second.CompareTo(Second);
  }
// Оператор равенства...
}

Для ясности я поместил тело основного конструктора в первый член класса. Однако в C# это не обязательно. Тело основного конструктора может появляться в любом порядке относительно других членов класса.

Тела основных конструкторов указывают поведение этих конструкторов.

Другая функциональность свойств только для чтения, хотя в CTP3 она пока не работает, заключается в том, что вы можете напрямую присваивать им значения из конструктора (например, First = first). Эта возможность не ограничивается основными конструкторами — она доступна для любого конструктора члена (constructor member).

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

В методе CompareTo есть параметры first и second, явно перекрывающиеся с именами параметров основного конструктора. Поскольку имена параметров основного конструктора находятся в области видимости инициализаторов автоматически реализуемых свойств, first и second могут показаться неоднозначными. К счастью, это не так. Правила областей видимости (scoping rules) опираются на другое измерение, которого еще не было в C#.

До C# 6.0 область видимости (scope) всегда идентифицировалась местонахождением в коде объявления переменной. Параметры связаны с методом, который они помогают объявить, поля — с классом, а переменные, объявленные в выражении if, ограничены телом условия этого выражения.

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

Параметры основного конструктора ограничены временем.

Однако инициализаторы автоматически реализуемых свойств реализуются так же, как и инициализаторы полей, которые транслируются в выражения, выполняемые в процессе инициализации класса в версиях C# 1.0+. Иначе говоря, область видимости параметра основного конструктора ограничена сроком жизни инициализатора класса и тела основного конструктора. Любая ссылка на параметры основного конструктора вне инициализатора автоматически реализуемого свойства или тела основного конструктора приведет к ошибке компиляции.

Важно знать и о нескольких дополнительных концепциях, относящихся к основным конструкторам. Только основной конструктор (primary constructor) может вызывать базовый конструктор (base constructor). Для этого вы используете ключевое слово base (контекстно-зависимое), за которым следует объявление основного конструктора.

Если вы указываете дополнительные конструкторы, цепочка вызовов конструкторов должна запускать основной конструктор последним. То есть у основного конструктора не может быть инициализатора this. У всех остальных конструкторов такие инициализаторы должны быть, предполагая, что основной конструктор не является заодно и конструктором по умолчанию:

public class Patent(string title, string yearOfPublication)
{
  public Patent(string title, string yearOfPublication,
    IEnumerable<string> inventors)
    ...this(title, yearOfPublication)
  {
    Inventors.AddRange(inventors);
  }
}

Хотелось бы надеяться, что эти примеры помогут продемонстрировать, что основные конструкторы делают язык C# проще. Это дополнительная возможность делать простые вещи простым, а не сложным способом. Иногда этим оправдывается наличие в классах множества конструкторов и цепочек вызовов, которые затрудняют чтение кода. Если вы встречаете ситуацию, где синтаксис основного конструктора усложняет ваш код вместо того, чтобы упрощать его, тогда не используйте такие конструкторы. Как и в случае всех усовершенствований в C# 6.0, если какое-то языковое средство вам не нравится или затрудняет чтение кода, просто откажитесь от него.

Функции и свойства в теле выражений

Функции в теле выражений (expression bodied functions) — это еще один синтаксис для упрощения в C# 6.0. Это функции, у которых отсутствует тело оператора (statement body). Вместо этого вы реализуете их с помощью выражения, которое следует за объявлением функции.

Например в класс Pair<T> можно было бы добавить переопределение ToString:

public override string ToString() => string.Format("{0}, {1}", First, Second);

В таких функциях нет ничего особо радикального. Как и большинство средств в C# 6.0, они обеспечивают упрощенный синтаксис для тех случаев, где реализация элементарна. Конечно, тип, возвращаемый выражением, должен соответствовать возвращаемому типу, указанному в объявлении функции. В данном случае ToString возвращает string, как и функция, реализованная выражением. Методы, возвращающие void или Task, следует реализовать выражениями, которые ничего не возвращают.

Такое упрощение не ограничивается функциями. С помощью выражений также можно реализовать свойства только для чтения (имеющие только аксессор get) — они называются свойствами в теле выражений (expression bodied properties). Например, можно добавить в класс FingerPrint член Text:

public string Text =>
  string.Format("{0}: {1} - {2} ({3})", TimeStamp, Process, Config, User);

Другие средства

Существует несколько средств, которые больше не планируется включить в C# 6.0.

  • Оператор индексируемого свойства ($) больше не доступен и не ожидается в C# 6.0.
  • Синтаксис члена индекса (index member) не работает в CTP3, но ожидается, что его вернут в более позднем выпуске C# 6.0:
var cppHelloWorldProgram = new Dictionary<int, string>
{
[10] = "main() {",
[20] = "    printf(\"hello, world\")",
[30] = "}"
};
  • Аргументы-поля в основных конструкторах больше не являются частью C# 6.0.
  • Как двоичный числовой литерал, так и разделитель чисел ('_') внутри числового литерала в настоящее время находятся под вопросом, удастся ли их доделать к моменту официального выпуска.

Есть также ряд средств, которые здесь не обсуждались, потому что я уже рассказывал о них в своей статье в номере за май, но статические выражения using (см. itl.tc/?p=4038), выражения объявлений (см. itl.tc/?p=4040) и улучшения в обработке исключений (см. itl.tc/?p=4042) — это средства, которые остаются в стабильном состоянии.

Заключение

Очевидно, что разработчики любят C# и хотят, чтобы его совершенство сохранялось на должном уровне. Группа, занимающаяся этим языком, серьезно относится к вашим замечаниям и предложениям и корректирует язык в соответствии с ними. Без колебаний заходите на сайт roslyn.codeplex.com и сообщайте этой группе свои соображения. Кроме того, не забывайте проверять itl.tc/csharp6 на предмет обновлений C# 6.0 до его официального выпуска.

Автор: Марк Микейлис  •  Иcточник: msdn.microsoft.com  •  Опубликована: 25.09.2015
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER
Теги:  


Оценить статью:
Вверх
Комментарии посетителей RSS

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