Введение
C# (произносится как «Си-шарп» или «Си-диез») — это простой, современный, строго типизированный объектно-ориентированный язык программирования. C# базируется на семействе языков программирования C и будет хорошо знаком программистам, работавшим с языками C, C и Java.
C# представляет собой объектно-ориентированный язык программирования, однако также обеспечивает поддержку компонентно_ориентированного программирования. Разработка современных приложений все в большей степени базируется на применении программных компонентов в форме автономных и самодокументируемых функциональных модулей. Основной особенностью таких компонентов является реализация модели программирования с использованием свойств, методов, событий и атрибутов, представляющих декларативное описание компонентов, а также включение в них собственной документации. В C# представлены языковые конструкции, непосредственно поддерживающие эти понятия, что делает его близким к естественному языком для создания и применения программных компонентов.
В C# представлены функциональные возможности, позволяющие создавать надежные и устойчивые приложения. Среди них: функция сборки мусора для автоматического освобождения памяти, занимаемой неиспользуемыми объектами; функция обработки исключительных ситуаций, обеспечивающая структурированный и расширяемый подход к обнаружению и устранению ошибок; а также строго типизированная структура языка, не допускающая считывания неинициализированных переменных, выхода индекса массива за пределы допустимого диапазона или выполнения непроверенных приведений типов.
В C# применяется унифицированная система типов. Все типы C#, включая простые типы (например, int и double), наследуются от единственного корневого типа object. Таким образом, все типы используют набор общих операций, что обеспечивает согласованные хранение, передачу и обработку значений любого типа. Кроме того, в C# поддерживаются пользовательские ссылочные типы и типы значений, что обеспечивает динамическое размещение объектов в памяти и встроенное хранение упрощенных структур.
Чтобы обеспечить совместимость и возможность дальнейшего развития программ и библиотек C#, в языке C# большое внимание уделяется управлению версиями. В большинстве языков программирования этому вопросу уделяется недостаточное внимание, в результате чего в создаваемых на таких языках программах чаще обычного возникают проблемы при переходе на новые версии зависимых библиотек. В C# реализованы следующие возможности по управлению версиями: разделение модификаторов virtual и override, применение правил разрешения перегрузки метода и поддержка явного объявления членов интерфейса.
Свойства
Свойства представляют собой естественные расширения полей. Как свойства, так и поля являются именованными членами со связанными типами, для обращения к которым используется одинаковый синтаксис. Однако, в отличие от полей, свойства не указывают места хранения. Вместо этого свойства содержат методы доступа, определяющие операторы, которые используются при чтении или записи их значений.
Свойство объявляется аналогично полю, однако объявление свойства должно заканчиваться не точкой с запятой, а методами доступа get или set, записанными между разделителями — { и }. Свойство, для которого определены оба метода доступа get и set, называется свойством для чтения и записи. Свойство, для которого определен только метод доступа get, называется свойством только для чтения. Свойство, для которого определен только метод доступа set, называется свойством только для записи.
Метод доступа get соответствует не содержащему параметров методу, возвращаемое значение которого имеет тип свойства. За исключением случаев, когда свойство является конечным объектом операции присваивания, при ссылке на свойство в выражении вызывается метод доступа get для вычисления значения свойства.
Метод доступа set соответствует методу с одним параметром value, не имеющему типа возвращаемого значения. При ссылке на свойство как на конечный объект операции присваивания или как на операнд операторов «» и «--» метод доступа set вызывается с аргументом, который предоставляет новое значение.
В классе List<T> объявляются два свойства: Count и Capacity (только для чтения и только для записи соответственно). В следующем примере показано использование этих свойств.
List<string> names = new List<string>();
names.Capacity = 100; // Вызывается метод доступа set
int i = names.Count; // Вызывается метод доступа get
int j = names.Capacity; // Вызывается метод доступа get
Как и в случае с полями и методами, в C# поддерживаются свойства экземпляров и статические свойства. Свойства, объявленные с использованием модификатора static, называются статическими. Все остальные свойства называются свойствами экземпляров.
Методы доступа свойства могут быть виртуальными. Если объявление свойства содержит модификатор virtual, abstract или override, соответствующий тип применяется и к его методам доступа.
Делегаты
Тип делегата представляет собой ссылки на методы с конкретным списком параметров и типом возвращаемого значения. С помощью делегатов методы обрабатываются как сущности, которым можно передавать параметры и которые можно присваивать переменным. Понятие делегата близко к понятию указателя на функцию, используемому в некоторых других языках. Однако делегаты, в отличие от указателей на функции, представляют собой пример объектно-ориентированного и строго типизированного подхода к программированию.
В следующем примере объявляется и используется тип делегата Function.
delegate double Function(double x);
Function f = Math.Abs;
double result = f(-5); // Результат: result = 5
Экземпляр типа делегата Function может ссылаться на любой метод, который принимает аргумент и возвращает значение типа double.
Делегат может ссылаться как на статический метод, так и на метод экземпляра. Делегат, ссылающийся на метод экземпляра, также ссылается на конкретный объект. При вызове такого метода экземпляра с помощью делегата этот объект становится объектом this.
Интересной и полезной особенностью делегата является то, что для него неважен тип класса или метода, на который он ссылается. Единственным требованием является наличие у метода, на который ссылается делегат, такого же числа параметров и типа возвращаемого значения.
События
Событие — это член, используемый классом или объектом для уведомления. Событие объявляется аналогично полю, однако оно должно иметь тип делегата и его объявление должно содержать зарезервированное слово event.
Если событие не является абстрактным и не содержит объявления методов доступа, его поведение в классе, в котором оно объявлено, аналогично поведению поля. В поле хранится ссылка на делегат, который представляет обработчики событий, добавленные к событию. Если обработчики событий отсутствуют, поле имеет значение null.
В классе List<T> объявляется член события Changed, указывающий на добавление нового элемента в список. Событие Changed вызывается виртуальным методом OnChanged, в котором сначала проверяется, имеет ли событие значение null (т. е. для события отсутствуют обработчики). Понятие вызова события в точности соответствует вызову делегата, представленного этим событием. Поэтому в языке не предусмотрены специальные конструкции для вызова событий.
Реакция клиента на событие реализуется с помощью обработчиков событий. Для вложения обработчиков событий используется оператор «+=», для удаления — оператор «-=». В следующем примере к событию Changed класса List<string> присоединяется обработчик событий.
using System;
class Test
{ static int changeCount;
static void OnListChanged(object sender, EventArgs e) {
changeCount++;
}
static void Main() {
List<string> names = new List<string>();
names.Changed += new EventHandler(OnListChanged);
names.Add("Анна");
names.Add("Екатерина");
names.Add("Светлана");
Console.WriteLine(changeCount); // Результат: "3"
}
}
Анонимные функции с поддержкой замыканий
Анонимная функция— это выражение, представляющее собой встроенное определение метода. Анонимная функция не имеет значения сама по себе, но может быть преобразована в совместимый тип делегата или дерева выражения. Преобразование анонимной функции зависит от целевого типа преобразования. Если это тип делегата, то результатом преобразования является значение делегата, ссылающееся на метод, определяемый анонимной функцией. Если это тип дерева выражения, то результатом преобразования является дерево выражения, которое представляет структуру метода в виде структуры объекта.
По историческим причинам существует две синтаксические разновидности анонимных функций, а именно: лямбдавыражения и выраженияанонимныхметодов. Практически для любых целей лямбдавыражения более конкретны и точны, чем выраженияанонимныхметодов, которые остаются в языке в целях обратной совместимости.
лямбда-выражение:
подпись анонимной функции => тело анонимной функции
выражение анонимного метода:
delegate явная подпись анонимной функциинеобяз блок
подпись анонимной функции:
явная подпись анонимной функции
неявная подпись анонимной функции
явная подпись анонимной функции:
( явный список параметров анонимной функциинеобяз )
явный список параметров анонимной функции
явный параметр анонимной функции
явный список параметров анонимной функции,
явный параметр анонимной функции
явный параметр анонимной функции:
модификатор параметра анонимной функции необяз тип идентификатор
модификатор параметра анонимной функции:
ref
out
неявная подпись анонимной функции:
( неявный список параметров анонимной функциинеобяз )
неявный параметр анонимной функции
неявный список параметров анонимной функции
неявный параметр анонимной функции
неявный список параметров анонимной функции ,
неявный параметр анонимной функции
неявный параметр анонимной функции:
идентификатор
тело анонимной функции:
выражение
блок
Оператор => имеет такой же приоритет, как и присваивание (=) и обладает правой ассоциативностью.
Тип параметров анонимной функции в виде лямбда_выражения может задаваться явно или неявно. В списке явно типизированных параметров тип каждого параметра указывается явно. В списке неявно типизированных параметров типы параметров выводятся из контекста, в котором находится анонимная функция, в частности, когда анонимная функция преобразуется в совместимый тип делегата или дерева выражения, типы параметров предоставляются этим типом.
В анонимной функции с одним параметром с неявной типизацией в списке параметров можно опустить скобки. Другими словами запись анонимной функции вида ( параметр ) => выражение можно сократить до параметр => выражение.
Список параметров анонимной функции в виде выраженияанонимногометода является необязательным. Если они заданы, то параметры должны быть явно типизированы. Если они не задаются, то анонимную функцию можно преобразовать в делегат с любым списком параметров, не содержащих параметров out.
Ниже приведены некоторые примеры анонимных функций.
x => x + 1 // Неявная типизация, тело выражения
x => { return x + 1; } // Неявная типизация, тело оператора
(int x) => x + 1 // Явная типизация, тело выражения
(int x) => { return x + 1; } // Явная типизация, тело оператора
(x, y) => x * y // Несколько параметров
() => Console.WriteLine() // Без параметров
delegate (int x) { return x + 1; } // Выражение анонимного метода
delegate { return 1 + 1; } // Список параметров опущен
Пример использования:
{
public class Rational
{ int _numerator;
int _denominator;
...
public static explicit operator double(Rational x)
{
return (double)x._numerator / x._denominator;
}
}
List<Rational> list = new List<Rational>();
... // Заполнение list.
list.Sort((r1, r2) => ((double)r1).CompareTo(r2));
}
Атрибуты
Типы, члены и другие сущности C# поддерживают модификаторы, которые управляют определенными аспектами их поведения. Например, доступность метода управляется с помощью модификаторов public, protected, internal и private. Благодаря этой возможности в C# пользовательские типы декларативных сведений могут быть вложены в сущности программы и извлекаться во время выполнения. Такие дополнительные декларативные сведения задаются в программе посредством определения и использования атрибутов.
В следующем примере атрибут HelpAttribute присоединяется к сущностям программы и предоставляет ссылки на связанную с ними документацию.
using System;
public class HelpAttribute : Attribute
{ string url;
public HelpAttribute(string url) { this.url = url; }
public string Url { get { return url; } }
}
Все классы атрибутов наследуются от базового класса System.Attribute, предоставляемого платформой .NET Framework. Чтобы применить атрибут, необходимо указать его имя и любые другие аргументы в квадратных скобках непосредственно перед связанным объявлением. Если имя атрибута заканчивается словом Attribute, при ссылке на него эту часть имени можно опустить. Например, атрибут HelpAttribute можно использовать следующим образом.
[Help("http://msdn.microsoft.com/.../MyClass.htm")]
public class Widget
{ [Help("http://msdn.microsoft.com/.../MyClass.htm", Topic = "Display")]
public void Display(string text) {}
}
В этом примере атрибут HelpAttribute присоединяется к классу Widget, а другой атрибут HelpAttribute — к методу Display класса.
Исключения
Исключения в языке C# обеспечивают структурированный, единообразный и строго типизированный способ обработки состояний ошибки, как на системном уровне, так и на уровне приложения. Механизм исключения в языке C# вполне сходен с механизмом в языке C, с несколькими важными отличиями:
- в C# все исключения должны быть представлены экземпляром типа класса, производным от System.Exception. В C для представления исключения может использоваться любое значение любого типа;
- в C# блок finally может использоваться для записи кода завершения, который выполняется как при нормальном выполнении, так и при исключительных состояниях. Такой код труден для написания в C без дублирования кода;
- в C# исключения системного уровня, такие как переполнение, деление на нуль и разыменование null, имеют хорошо определенные классы исключений и находятся на одном уровне с состояниями ошибки уровня приложения.
Причины исключений
Исключения могут вызываться двумя разными способами.
Оператор throw вызывает исключение немедленно и безусловно. Управление никогда не передается оператору, следующему за оператором throw.
Определенные исключительные состояния, возникающие при обработке операторов и выражения C#, служат причиной исключения в определенных обстоятельствах, когда операцию не удается выполнить нормально. Например, операция целочисленного деления создает исключение System.DivideByZeroException, если знаменатель равен нулю.
Обработка исключений
Исключения обрабатываются оператором try.
Когда происходит исключение, система ищет ближайшее предложение catch, которое может обработать исключение, как это определено типом времени выполнения исключения. Сначала в текущем методе выполняется поиск лексически объемлющего оператора try, и связанные предложения catch оператора try рассматриваются по порядку. Если это не удается, в методе, вызвавшем текущий метод, выполняется поиск лексически объемлющего оператора try, который содержит точку вызова текущего метода. Этот поиск продолжается, пока не будет найдена конструкция catch, которая может обработать текущее исключение, назвав имя класса исключения такого же класса или базового класса в выполняемом типе созданного исключения. Конструкция catch, которая не именует класс исключения, может обрабатывать любое исключение.
Когда соответствующая конструкция catch найдена, система подготавливает передачу управления первому оператору конструкции catch. Прежде чем начинается выполнение конструкции catch, система сначала последовательно выполняет все предложения finally связанные с операторами try с большим уровнем вложенности, чем тот оператор, который вызвал исключение.
Если соответствующая конструкция catch не найдена, выполняется одно из следующих двух действий:
- если поиск соответствующей конструкции catch доходит до статического конструктора или инициализатора статического поля, создается исключение System.TypeInitializationException в точке, запустившей вызов статического конструктора. Внутреннее исключение System.TypeInitializationException содержит то исключение, которое возникло первоначально;
- если поиск соответствующих предложений catch достигает кода, который первоначально запустил поток, выполнение потока завершается. Влияние такого завершения определяется реализацией.
Исключения, происходящие при выполнении деструктора, заслуживают особого упоминания. Если исключение происходит при выполнения деструктора и не перехватывается, выполнение этого деструктора завершается и вызывается деструктор базового класса (если он имеется). Если нет базового класса (как в случае типа object) или нет деструктора базового класса, исключение удаляется.
Итераторы
Итераторы в .NET Framework называются 'перечислителями' (enumerators) и представлены интерфейсом IEnumerator.
IEnumerator реализует:
- метод MoveNext(), который переходит к следующему элементу и указывает достигнут ли конец коллекции;
- свойство Current служит для получения значения указываемого элемента;
- дополнительный метод Reset() возвращает перечислитель на его исходную позицию.
Перечислитель первоначально указывает на специальное значение перед первым элементом, поэтому вызов MoveNext() необходим для начала итерации.
Перечислители обычно передаются вызовом метода GetEnumerator() объекта, реализующего интерфейс IEnumerable. Классы контейнеров обычно реализуют этот интерфейс. Тем не менее, выражение foreach в языке C# может оперировать любым объектом, поддержвающим подобный метод, даже если он не реализует IEnumerable. Оба интерфейса были расширены в обобщенных версиях .NET 2.0 .
Следующий пример показывает простое использование итераторов в C# 2.0:
// 'явная' версия
IEnumerator<MyType> iter = list.GetEnumerator();
while (iter.MoveNext())
Console.WriteLine(iter.Current);
// 'неявная' версия
foreach (MyType value in list)
Console.WriteLine(value);
C# 2.0 также поддерживает генераторы: метод, объявляемый как возвращаемый IEnumerator (или IEnumerable), но использующий выражение "yield return" (гибкое возвращение) для создания последовательности элементов вместо возвращения сущности объекта, будет превращен компилятором в новый класс, реализующий соответствующий интерфейс.
Перегрузка операторов
Оператор — это член, который определяет значение применения конкретного оператора выражения к экземплярам класса. Поддерживается определение операторов трех видов: унарные операторы, двоичные операторы и операторы преобразования. Все операторы должны объявляться с использованием модификаторов public и static.
У всех унарных и бинарных операторов есть стандартная реализация, доступная автоматически в любом выражении. В дополнение к стандартным реализациям объявление operator в классах и структурах позволяет использовать пользовательские реализации. Пользовательские реализации операторов всегда имеют больший приоритет по сравнению со стандартными реализациями операторов.
К унарным операторам, допускающим перегрузку, относятся:
- ! ~ + -- true false
Несмотря на то что true и false явно в выражениях не используются, они считаются операторами, потому что вызываются в некоторых контекстах выражений: логических выражений и выражений с условными и условными логическими операторами.
К бинарным операторам, допускающим перегрузку, относятся:
+ - * / % & | ^ << >> == != > < >= <=
Перегрузка возможна только для операторов, указанных выше. В частности, невозможна перегрузка операторов доступа к членам, вызова методов или операторов =, &&, ||, ??, ?:, =>, checked, unchecked, new, typeof, default, as и is.
При перегрузке бинарного оператора связанный оператор присваивания (если он есть) также неявно перегружается. Например, при перегрузке оператора * также выполняется перегрузка оператора *=. Обратите внимание, что сам оператор присваивания (=) нельзя перегрузить. При присваивании всегда происходит простое побитовое копирование значения в переменную.
Перегрузка операций приведения типов, например (T)x, осуществляется путем предоставления пользовательских преобразований.
Доступ к элементу, например a[x], не считается оператором, допускающим перегрузку. Вместо этого поддерживается пользовательская индексация в индексаторах.
В пользовательских объявлениях операторов по крайней мере один из параметров всегда должен иметь тип класса или структуры, в котором содержится объявление оператора. Таким образом, у пользовательского оператора не может быть такой же подписи, что и у стандартного оператора.
public class Rational
{ int _numerator;
int _denominator;
public Rational(int numerator, int denominator)
{
_numerator = numerator;
_denominator = denominator;
}
public int Numerator { get { return _numerator; } }
public int Denominator { get { return _denominator; } }
public static Rational operator -(Rational x)
{
return new Rational(-x._numerator, x._denominator);
}
public static Rational operator ++(Rational x)
{
return new Rational(x._numerator + 1, x._denominator);
}
public static Rational operator *(Rational x, int y)
{
return new Rational(x._numerator * y, x._denominator);
}
public static Rational operator /(Rational x, Rational y)
{
return new Rational(x._numerator * y._denominator, x._denominator * y._numerator);
}
public static explicit operator double(Rational x)
{
return (double)x._numerator / x._denominator;
}
}
Комментарии в формате XML
C# предоставляет программистам механизм документирования своего кода с помощью специального синтаксиса комментариев с XML-текстом. Комментарии в файлах исходного кода, имеющие определенный вид, могут быть использованы для управления инструментом создания XML из этих комментариев и элементов исходного кода, которым они предшествуют. Комментарии, использующие такой синтаксис, называются комментариями к документации. Они должны непосредственно предшествовать пользовательскому типу (такому как класс, делегат или интерфейс) или члену (такому как поле, событие, свойство или метод). Инструмент создания XML называется генератором документации. (Таким генератором может быть, но не обязан, сам компилятор C#.) Производимый генератором документации вывод называется файлом документации. Файл документации используется в качестве входных данных для средства просмотра документации, инструмента для создания отображения сведений о типе и сопутствующей документации.
Введение
Комментарии, имеющие специальный вид, могут быть использованы для управления инструментом создания XML из этих комментариев и элементов исходного кода, которым они предшествуют. Такие комментарии являются однострочными комментариями, начинающимися с трех косых черт (///), или комментариями с разделителями, начинающимися с косой черты и двух звездочек (/). Они должны непосредственно предшествовать пользовательскому типу (такому как класс, делегат или интерфейс) или члену (такому как поле, событие, свойство или метод), который они комментируют.
Синтаксис:
однострочныйкомментарийк_документации:
/// символы_ввода необ
комментарийсразделителямикдокументации:
/** символыкомментарияс_разделителяминеоб */
Пример.
/// <summary>Класс <c>Point</c> моделирует точку в двумерной
/// плоскости.</summary>
public class Point
{ /// <summary>Метод <c>draw</c> изображает точку.</summary>
void draw() {…}
}
Текст в комментариях к документации должен быть правильным согласно правилам XML (http://www.w3.org/TR/REC-xml). При неправильном XML создается предупреждение и файл документации будет содержать комментарий, сообщающий об ошибке.
Рекомендуемые теги
Генератор документации обязан принять и обработать любой тег, допустимый по правилам XML. Следующие теги предоставляют обычно используемую функциональность в пользовательской документации. (Конечно, возможны и другие теги.)
|| Тег || Назначение ||
| <c> | Установить шрифт текста, подобный исходному коду |
| <code> | Установить одну или несколько строк исходного кода или программного вывода |
| <example> | Указать пример |
| <exception> | Идентификация исключений, которые могут выдаваться методом |
| <include> | Включение XML из внешнего файла |
| <list> | Создание списка или таблицы |
| <para> | Разрешить добавление структуры к тексту |
| <param> | Описать параметр для метода или конструктора |
| <paramref> | Указать, что слово является именем параметра |
| <permission> | Документировать специальные возможности безопасности члена |
| <summary> | Описать тип |
| <returns> | Описать возвращаемое методом значение |
| <see> | Указать ссылку |
| <seealso> | Создать запись See Also |
| <summary> | Описать член типа |
| <value> | Описать свойство |
| <typeparam> | Описать параметр универсального типа |
| <typeparamref> | Указать, что слово является именем параметра типа |
Обобщённые типы и методы
Объявление универсального типа определяет несвязанный универсальный тип, который используется в качестве шаблона для формирования различных типов посредством применения аргументов типа. Аргументы типа записываются с помощью угловых скобок («<» и «>») непосредственно после имени универсального типа. Тип, содержащий как минимум один аргумент типа, называется сформированным типом. Сформированный тип может использоваться в большинстве конструкций языка, в которых используется имя типа. Несвязанный универсальный тип может использоваться только в выражении_typeof.
Сформированные типы также могут использоваться в выражениях в качестве простых имен или при доступе к членам.
При вычислении пространстваименилиименитипа рассматриваются только универсальные типы, содержащие допустимое число параметров. Таким образом, возможно обозначение типов, имеющих различное число параметров, с помощью одного идентификатора. Это полезно при одновременном использовании в программе универсальных классов и классов, не являющихся таковыми:
namespace Widgets
{ class Queue {...}
class Queue<TElement> {...}
}namespace MyApplication
{ using Widgets;
class X
{
Queue q1; // неуниверсальный Widgets.Queue
Queue<int> q2; // универсальный Widgets.Queue
}
}
Имя_типа должно определять сформированный тип, даже если в нем прямо не заданы параметры типа. Это может произойти, если тип является вложенным в объявлении универсального класса, а тип экземпляра в содержащем объявлении неявно используется для поиска имен:
class Outer<T>
{ public class Inner {...}
public Inner i; // типом i является Outer<T>.Inner
}
В небезопасном коде не допускается использование сформированного типа в качестве неуправляемого_типа.