Структура программы на С# Любая программа на С# - это набор классов, структур и других определенных программистом типов. В NET Framework 1.0 – 1.1 в одном файле может быть определено несколько типов, но каждый тип должен быть определен в одном файле. // A.cs public class A { // Поля данных // и методы } public struct B { // Поля данных // и методы } // C.cs public class C { // Поля данных // и методы } // M.cs public class App { public static void Main(string[] args) public enum D { // Константы } { // Точка входа } } A.cs + B.cs = lib.dll A.cs + M.cs = app.exe Частичные(partial) классы (.NET Framework 2.0) Можно определить тип в разных файлах. Если тип определяется в разных файлах, перед каждым объявлением типа указывается ключевое слово partial. // файл Abc.cs public partial class Abc { private string key; public virtual string ToString() { return “key=“ + key; } } // файл Abc1.cs public partial class Abc { public string Key { get() { return key;} set() { key = value;} } } Пространства имен Позволяют избежать совпадения имен в больших проектах namespace A { class Cls { … } } namespace B { class Cls { … } } A.Cls obj1 = new A.Cls(); B.Cls obj2 = new B.Cls(); Не связаны с физическим расположением классов A B c1.dll c2.dll Пространства имен -2 Можно использовать сокращенную запись using A; Cls obj = new Cls(); // A.Cls obj = A.Cls(); Можно вводить псевдонимы using Btn = System.Windows.Forms.Button; Btn b = new Btn(); // System.Windows.Forms.Button b = new System.Windows.Forms.Button(); Директиву using можно использовать только в начале файла или пространства имен Вложенные пространства имен. Пространства имен могут быть вложены namespace A { namespace B { class Cls { … } } } namespace A.B { class Cls { … } } using для объемлющего пространства имен не означает сокращение имен для вложенных using. using A; using A.B; A.B.Cls obj1; B.Cls obj2; // Ошибка A.B.Cls obj1; Cls obj2; // OK. Классы и структуры в C# Класс (class) – ссылочный тип. Структура (struct) – тип-значение. Структура не может • иметь деструктор; • иметь конструктор без параметров; • использоваться как базовый тип. Описание класса в C# модификатор_доступа class имя-класса { … описание данных и методов … } Модификаторы доступа для классов: public internal - класс доступен из других сборок; - класс видим только внутри данной сборки; Тип доступа по умолчанию – internal. Члены класса • Константы • Поля (field) • Конструкторы (в том числе без параметров) • Деструктор • Методы (статические и экземплярные) • Свойства (property) • Индексаторы (свойства с параметрами) • События (event) • Вложенные типы Доступ к членам класса (полям и методам) Модификатор Доступ public в любом методе любой сборки protected в методах самого класса, в методах производных классов любой сборки internal в любом методе данной сборки protected internal в любом методе данной сборки, в производных классах других сборок private только в методах самого класса Вложенный тип может иметь любой из 5 модификаторов доступа, но уровень доступа не может быть выше уровня доступа объемлющего типа. Константы Локальные переменные, поля классов и структур могут иметь модификатор const. Константы инициализируются при объявлении. Константы применяются для улучшения читаемости кода. public class TextEditor { const int MaxLines = 10000; … } Поля readonly Поля классов и структур могут иметь модификатор readonly. Поле с модификатором readonly инициализируется в конструкторе и больше не изменяется. readonly поля могут инициализироваться явно public class TextEditor { readonly int MaxLines = 10000; … } readonly поля могут инициализироваться по умолчанию public class TextEditor { readonly int MaxLines; // Значение 0 … } readonly поля могут инициализироваться в конструкторе public class TextEditor { readonly int MaxLines; public TextEditor(int maxLines) { MaxLines = maxLines; string[] lines = new string[MaxLines]; } … } Статические методы и данные Методы и поля данных объявляются как статические с помощью модификатора static. Статические данные совместно используются всеми экземплярами класса. Статический метод вызывается через класс. Статический метод может быть вызван еще до создания экземпляра (instance) класса. double res = Math.Sin(Math.PI/6); Console.WriteLine("res= {0}", res); StringBuilder str = new StringBuilder(res.ToString()); Console.WriteLine(str.Insert(0,"sin(pi/6)=")); Инициализация объектов: конструкторы При создании объекта выполняются два действия: • выделяется память под объект; • выделенный участок памяти инициализируется. T t = new T(); Выделение памяти Инициализация объекта Инициализацию выделенного участка памяти выполняет специальный метод класса – конструктор. В классе может быть определено несколько конструкторов с различными списками параметров. Конструктор по умолчанию Конструктор без параметров называется конструктором по умолчанию. Если конструктор по умолчанию не задан, компилятор сам создает конструктор по умолчанию. Если в классе определен хотя бы один конструктор, конструктор по умолчанию не создается. Когда нужен свой конструктор по умолчанию? • при инициализации объекта требуется выполнить действия более сложные, чем простое присваивание значений полям данных; • требуется изменить уровень доступа к конструктору. Singleton pattern Иногда требуется ограничить количество экземпляров объекта одним. public class Singleton { private Singleton() { // выполняем инициализацию } public static Singleton GetInstance() { if( instance == null) instance = new Singleton(); return instance; } private static Singleton instance; } // Singleton s = new Singleton(); // Ошибка! Singleton s2 = Singleton.GetInstance(); // OK. Списки инициализаторов Чтобы избежать дублирования кода, можно вызывать один конструктор из другого при помощи специальной синтаксической конструкции - списка инициализаторов. public T(string key) { this.key = key; nobj++; } public T(string key, double []dt) : this(key) { this.dt = new double[dt.Length] ; Array.Copy(dt, this.dt, dt.Length); } Список инициализаторов применим только в конструкторах и не может ссылаться сам на себя (рекурсивный вызов конструкторов). Статический конструктор При выполнении программы под управлением CLR классы могут загружаться во время выполнения программы. После загрузки класса, но перед его использованием всегда выполняется статический конструктор (конструктор класса), инициализирующий статические поля данных. public class T { static int st; static T() { // Инициализация статических полей } … } Свойства статического конструктора Для статического конструктора гарантируется, что: • Статический конструктор будет вызван перед созданием первого экземпляра объекта. • Статический конструктор будет вызван перед обращением к любому статическому полю класса или статическому методу класса. • Статический конструктор будет вызван не более одного раза за все время выполнения программы. Статический конструктор не может иметь модификатор доступа. Статический конструктор не может принимать параметры. Статический конструктор не может быть вызван явно. Свойства Свойство - пара методов со специальными именами: • метод get() вызывается при получении значения свойства; • метод set() вызывается при задании значения свойства. Каждое свойство имеет имя и тип. Если определен только метод get , свойство доступно только для чтения. Если определен только метод set, свойство доступно только для “записи”. Свойство может быть связано с закрытым полем класса. Свойства работают немного медленнее прямого доступа к полю. Cвойства - 2 public class T { string key; double [] dt; public string Key { get { return key; } set { key = value; } } public double Sum { get { double s = 0; foreach(double d in dt) s +=d; return s; } } ... } T t = new T(); t.Key = “abcd”; double s = t.Sum; Зачем нужны свойства? • При помощи свойств можем выполнять свой код при изменении объекта или запросе состояния объекта. • Свойства реализуют принцип инкапсуляции. • Свойства активно используются визуальными инструментами разработки. Индексаторы (свойства с параметрами) В С# для определения оператора индексирования [ ] используется специальный синтаксис. Индексатор имеет определенный тип. Индексатор может быть перегружен – можно определить индексаторы с различным числом и типами параметров. public { get set } public { get set } double this[int j] { return dt[j]; } { dt[j] = value;} double this[int i, int j] {return dt[Math.Abs(i-j)];} {dt[Math.Abs(i-j)] = value;} Передача параметров размерных типов struct S {…}; class Class1 { void F (S p1, ref S p2, out S p3) {...} ... } p1 p2 и p3 p2 p3 передается по значению - делается ограниченная (shallow) копия, изменения не возвращаются в контекст вызова; передаются по ссылке, изменения возвращаются в контекст вызова; перед вызовом должен быть инициализирован; может быть не инициализирован, в методе должно быть присвоено значение. Передача параметров ссылочных типов class Class1 { public static void f1 (double[] darr) // аналог в C++ double* { darr[0] = 555; double[] df = new double[]{5,15}; darr = df; } public static void f2 (ref double[] darr) // аналог в C++ double* & { darr[0] = 777; double[] df = new double[]{7,17}; darr = df; } static void Main(string[] args) { double[]dm = {1,2,3}; Class1.f1(dm); foreach(double d in dm) Console.Write(" {0}", d); // 555 2 3 Class1.f2(ref dm); foreach(double d in dm) Console.Write(" {0}", d); // 7 17 } } f1 делается копия ссылки dm, при выходе из f1 значение dm не изменится, но элементы массива изменятся f2 при выходе из f2 значение dm изменится Методы с переменным числом параметров class Class1 { ... public static void f (T1 p1, T2 p2, params T[] p) {...} } Только последний параметр метода может иметь модификатор params. Параметр c модификатором params должен быть одномерным массвом (любого типа). Если при вызове не найден метод с точным совпадением типов фактических параметров, параметры собираются в массив и вызывается метод с модификатором params. Перегрузка операторов для классов и структур Возможна перегрузка бинарных операторов: + - * % / | & ~ << >> и унарных операторов + - ~ ! ++ - Операторы перегружаются с помощью открытых статических методов. Один из параметров должен совпадать с объемлющим типом. Операторы (составные операции присваивания) += -= *= /= %= >>= <<= &= |= ^= не могут быть перегружены, но когда перегружены соответствующие им бинарные операции, последние используются при вычислении выражений с операторами += -= *= /= %= >>= <<= &= |= ^= Перегрузка операторов. Пример struct Rational { int a,b; public Rational(int aa, int bb) { a = aa; b = bb; } // бинарный + public static Rational operator+ (Rational ls, Rational rs) { return new Rational (ls.a*rs.b + ls.b*rs.a , ls.b*rs.b); } // унарный public static Rational operator- (Rational r) { return new Rational(-r.a, r.b); } } static void Main(string[] args) { Rational r1 = new Rational(1,2); Rational r2 = new Rational(1,3); Rational r3 = r1 + r2; r2 += r1; // Вычисляется как r2 = r2 + r1; Rational r4 = -r1; } Перегрузка операторов ++ и -Операторы ++ и -- могут применяться в префиксной и постфиксной форме: r1 = ++a1; // Вычисляется как a1 = operator ++ (a1); r1 = a1; r2 = a2++; // Вычисляется как r2 = a2; a2 = operator ++ (a2); struct Rational { int a,b; public Rational(int aa,int bb) { a = aa; b = bb; } public static Rational operator ++ (Rational r) { return new Rational(r.a + r.b, r.b); } public override string ToString() { return "(" + a + "," + b +")"; } } static void Main(string[] args) { Rational a1 = new Rational(1,2); Rational a2 = new Rational(1,2); Rational r1 = ++a1; Console.WriteLine("a1={0} r1={1}", a1, r1);// a1=(3,2) r1=(3,2) Rational r2 = a2++; Console.WriteLine("a2={0} r2={1}", a2, r2);// a2=(3,2) r2=(1,2) } Перегрузка операторов сравнения Перегрузка операторов сравнения возможна только в парах: <= и >= <и> == и != Чтобы избежать дублирования кода и связанных с ним ошибок, рекомендуется поместить код для сравнения в единственный статический метод и вызывать его из методов, перегружающих операторы. struct Rational { int a,b; … public static int Compare(Rational ls, Rational rs) { long d = ls.a*(long)rs.b - rs.a*(long)ls.b; if(d < 0) return -1; // Первый аргумент меньше второго else if(d > 0) return 1; // Первый аргумент больше второго else return 0; } public static bool operator < (Rational ls, Rational rs) { return (Compare(ls,rs) < 0); } public static bool operator > (Rational ls, Rational rs) { return (Compare(ls,rs) > 0); } } Операторы преобразования типов Можно объявлять явные и неявные преобразования для структуры или класса как статические методы. struct Rational{ Оператор явного преобразования bool->Rational int a,b; public static explicit operator Rational ( bool val ) { int a = val ? 1 : -1; return new Rational( a,1); } Оператор неявного преобразования int->Rationall public static implicit operator Rational ( int val ) { return new Rational(val,1); } public static explicit operator bool ( Rational r ) { return r.a*r.b > 0; } … } static void Main(string[] args) { Rational r1 = 5; bool b1 = true; Rational r2 = (Rational) b1; bool b2 = ( bool) r1; } Жизненный цикл объекта Создание объекта при вызове оператора new • Под объект выделяется область памяти из управляемой кучи. • Область памяти инициализируется при работе конструктора. • Возможно захватывается ресурс, например, открывается файл или устанавливается соединение с базой данных. Использование объекта • Вызываются методы объекта, производится доступ к полям данных объекта. Уничтожение объекта (точное время не определено) • При помощи деструктора (финализатора) объект превращается в область памяти. • Область памяти возвращается системе. Освобождением занятых ресурсов занимается сборщик мусора. Деструктор В классе можно определить деструктор. В отличие от C++ порядок вызова деструкторов в C# не определен. Перед освобождением памяти сборщик мусора вызывает деструктор, но возможны ситуации, когда деструктор для объекта не вызывается совсем. Определять свои деструкторы нужно только в случае крайней необходимости - они создают значительную нагрузку на систему. Деструкторы и метод Finalize На самом деле наличие в классе деструктора означает неявное переопределение метода Finalize class T { ~T() { Console.WriteLine(“T destructed”); } } class T { protected override void Finalize(){ try { Console.WriteLine(“T destructed”); } finally { base.Finalize(); } } } Сборщик мусора ( Garbage Collector) CLR использует модель сборки с поколениями 0 – (128-256-512 K) 1 – (2 Mб) 2 – (10 Mб) Сборщик мусора начинает работу, когда заполнено поколение 0, перемещая объекты в поколение 1. Для больших объектов (> 85000 байт) своя куча и свой алгоритм работы GC. Алгоритмы сборки отличаются в debug и release версиях и в различных версиях OC. Сборщик мусора ( Garbage Collector) - 2 Перед вызовом Ctor объекты с завершением (с Dtor) заносятся в специальный список объектов, для которых надо вызвать Dtor перед освобождением памяти. При сборке мусора в поколении 0 объекты с завершением не уничтожаются, а заносятся в специальную очередь завершения. Очередь завершения обрабатывает специальный высокоприоритетный поток. В каком случае Dtor может быть не выполнен? После завершения работы приложения CLR вызывает Finalize() для каждого объекта с завершением, но если время возврата из него более 2 сек, CLR завершает процесс и остальные методы Finalize() не вызываются.