3.1 Немного реального мира, программа вне среды разработки
Вопрос может показаться глупым опытным программистам - но где реально находится наша программа и как ее запустить вне среды разработки? В первой части мы выбрали каталог для проекта. Сама по себе программа по умолчанию и для проекта TestConsoleApplication будет находится в подкаталоге TestConsoleApplication\TestConsoleApplication\bin\Debug\TestConsoleApplication.exe и на данный момент состоять из одного-единственного файла TestConsoleApplication.exe. Его можно скопировать в другое место и запустить оттуда.
Итоговые файлы могут находиться и в другой папке, это зависит от конфигурации приложения.
Более подробно о различиях в конфигурации можно прочить в официальной документации Microsoft.
3.2 Обработка ошибок
Некоторые ошибки сразу видны среде разработки, которая не дает скомпилировать убогую программу (и про себя ругает кривые руки программиста). Но самые опасные ошибки происходят во время работы программы. Пример ошибки, которую не может заметить среда разработки - обращение к несуществующему элементу списка. Элементов всего 2, а мы пытаемся прочитать третий. Можно было бы усложнить задачу - в реальной жизни мы скорее всего получим извне список с неизвестным заранее числом элементов, но для тестового примера в этом нет смысла. Обратите внимание, что такая ошибка невозможна при использовании цикла foreach.
static void Main(string[] args) static void Main(string[] args) { List<Person> personsList = new List<Person>(); personsList.Add(new Person("Пушкин", "Александр", "Сергеевич")); personsList.Add(new Person("Гончарова", "Наталья", "Николаевна")); for (int counter = 0; counter < 4; counter++) { Console.WriteLine(personsList[counter].Fio); } Console.ReadLine(); }
В среде разработки такая ошибка выглядит так
и вернуться к редактированию кода можно так
А при запуске программы напрямую из exe-файла так
В чем философский смысл такой ошибки? Компьютер не знает что ему делать! Он же в конце-концов простая железяка и программист должен объяснить, как поступать в непредвиденной ситуации. Но для этого сам программист должен как-то узнать что именно происходит. Поэтому для ошибок в .Net и C# существует механизм исключений. Как несложно догадаться, ошибка, которую в среде .Net зовут исключением (Exception) - это тоже класс, экземпляр которого выбрасывается в случае вышеописанной ситуации, а программист должен ошибку поймать и что-сделать. Используется для этого конструкция try-catch
Внутри try помещается код, который может вызвать ошибку. Внутри catch код, который будет что-то делать с ошибкой. По идее внутрь try надо помещать поменьше кода, но в несколько уродливом примере ниже упор сделан на наглядность отображения событий, происходящих при возникновении исключительной ситуации.
static void Main(string[] args) { List<Person> personsList = new List<Person>(); personsList.Add(new Person("Пушкин", "Александр", "Сергеевич")); personsList.Add(new Person("Гончарова", "Наталья", "Николаевна")); for (int counter = 1; counter <= 2; counter++) { try { Console.WriteLine("Код ДО операции, в которой может произойти ошибка"); Console.WriteLine(personsList[counter].Fio); // в случае возникновении ошибки выполнение прервется на этом месте и перейдет в блок catch Console.WriteLine("Код ПОСЛЕ операции, в которой может произойти ошибка"); } catch (Exception error) { Console.WriteLine("Произошла ошибка! Информация об ошибке " + error.Message); } finally { Console.WriteLine("Код внутри необязательного блока finally выполнится вне зависимости от того, произошла ошибка или нет"); Console.WriteLine(); } } Console.ReadLine(); }
Само собой вместо банального вывода сообщения на экран можно писать сообщение в файл с журналом ошибок или пытаться как-то исправить ситуацию. Но даже такие простые действия спасают компьютер от экзистенциального ужаса при столкновении с неизвестным
Подробнее в документации Microsoft
3.3 Отладка
Ошибки из предыдущего раздела - это еще не самое страшное. По крайней мере компьютер четко говорит, что он не знает что делать. Мы знаем, где произошла ошибка и даже имеем примерное описание причины. Хуже всего, когда программа просто работает неправильно. Никаких ошибок не появляется, просто результат работы неправильный. Например, мы сделали невероятно мощный искусственный интеллект для спасения человечества, а он решает уничтожить всех людей.
Главный инструмент борьбы с этим ночным кошмаром миллионов людей - отладка. Проще говоря пошаговое выполнение программы начиная с определенного момента, и просмотр переменных по мере их изменения в памяти.
Как правило нам не нужно по шагам отлаживать всю программу, так что для входа в режим отладки в заданном месте используются точки остановки (breakpoints). В Блокноте отладка работать не будет, нам понадобится специальная программа-отладчик, которая входит в состав любой уважающей себя среды разработки, включая и Visual Studio. Устанавливаем точку основа на нужной строке кликая чуть левее начала строки и запускаем саму программу в режиме отладки, клавишей F5 или соответствующим пунктом меню
Ключевые клавиши - F10, позволяющая перейти в выполнении программы на следующую строку и F11 позволяющая при наличии в строке вызова функции перейти внутрь этой функции. Самой простой способ увидеть значения переменных - навести на них курсор.
Возможной альтернативой отладке является продвинутая методика разработки через тестирование, до сих пор вызывающая бурные споры.
3.4 Наследование
Данный самоучитель сильно отличается от академических курсов программирования и дает необходимый минимум для начала программирования на практике, не забывайте об этом и главной задаче программирования.
На практике разные классы очень часто пересекаются по функциональности. Новый класс или классы могут лишь изменять некоторые функции старых и/или добавлять к ним что-то, не использующееся в базовом. Чтобы не повторять одну и ту же функциональность в разных местах используется механизм наследования.
Один класс считается базовым. Второй, меняющий часть функциональности базового, считается его наследником. В C# наследовать сразу от нескольких базовых классов нельзя. Но у одного базового класса может быть сколько угодно наследников. Как и большинство продвинутых методик программирования, наследование крайне сложно иллюстрировать простыми примерами - очень уж надуманными и бессмысленными они кажутся. ООП упрощает написание больших и сложных программ - в них оно экономит время и силы разработчиков.
Предположим, что мы пишем программу для управления кораблем, на котором будут перевозить людей и кошек (а что еще надо для счастья?). Для людей создадим класс Person, для кошек Cat. Само собой люди отличаются от кошек даже на крайне примитивном уровне - у кошек нет фамилии и отчества, у людей нет хозяев... и так далее. Но для управления кораблем нам надо знать общий вес пассажиров, чтобы избежать перегрузки.
Таким образом можно создать базовый класс Cargo (Груз), в котором будет описана масса, а Person и Cat унаследовать от базового, после чего массу отдельно описывать не понадобится. Интересно, сколько весил Пушкин?
class Program { static void Main(string[] args) { List<Person> personsList = new List<Person>(); personsList.Add(new Person("Пушкин", "Александр", "Сергеевич", 60)); personsList.Add(new Person("Гончарова", "Наталья", "Николаевна", 40)); List<Cat> catsList = new List<Cat>(); catsList.Add(new Cat("Барсик", personsList[0], 10)); catsList.Add(new Cat("Багира", personsList[1], 8)); List<Cargo> cargoList = new List<Cargo>(); cargoList.Add(personsList[0]); cargoList.Add(personsList[1]); cargoList.Add(catsList[0]); cargoList.Add(catsList[1]); double totalMass = 0; foreach (Cargo currCargo in cargoList) { totalMass += currCargo.Mass; } Console.WriteLine("суммарная масса всех пассажиров включая и котов и людей: " + totalMass); Console.ReadLine(); } } public class Cargo { private double _mass = 0; public double Mass { get { return _mass; } } public Cargo(double mass) { _mass = mass; } } // класс человека public class Person : Cargo { // три строковых переменные-свойства, доступные извне класса - public private string _name = ""; public string Name { get { return _name; } set { _name = value; } } private string _surname = ""; public string Surname { get { return _surname; } set { _surname = value; } } private string _otchestvo = ""; public string Otchestvo { get { return _otchestvo; } set { _otchestvo = value; } } public string Fio { get { string fio = Surname + " " + Name + " " + Otchestvo; return fio; } } // конструктор, специальная функция, которая вызывается при создани экземпляра класса с помощью слова new public Person(string surname, string name, string otchestvo, double mass) : base(mass) { Name = name; Surname = surname; Otchestvo = otchestvo; } } public class Cat : Cargo { public string Name = ""; public Person Master = null; public Cat(string name, Person master, double mass) : base(mass) { Name = name; Master = master; } }
Обратите внимание на код
List<Cargo> cargoList = new List<Cargo>(); cargoList.Add(personsList[0]); cargoList.Add(personsList[1]); cargoList.Add(catsList[0]); cargoList.Add(catsList[1]);
Дочерний класс можно конвертировать в базовый. При этом в переменной типа базового класса будет храниться ссылка на объект дочернего класса, через нее будут доступны общие для них обоих функции и свойства. А вот обратная конвертация чревата ошибками.
Person person = personsList[0]; Cargo cargo = (Cargo)person; Person person2 = (Person)cargo; Cat cat = (Cat)cargo; // ошибка во время выполнения Unable to cast object of type 'TestConsoleApplication.Person' to type 'TestConsoleApplication.Cat'.
Наследники могут и менять какие-то функции базового класса - это называется переопределением. На самом деле все классы в C# наследуются от базового класса Object среди немногих методов которого присутствует метод ToString(). Но если мы попытаемся использовать его для вывода информации о наших пассажирах, то ничего хорошего не получится.
foreach (Cargo currCargo in cargoList) { Console.WriteLine(currCargo.ToString()); }
На самом деле именно этот метод неявно вызывается при скрытых преобразованиях в строку (в духе " масса = " + mass )
так что предыдущий пример можно записать короче, но правильно он от этого работать не начнет
foreach (Cargo currCargo in cargoList) { Console.WriteLine(currCargo); }
Но в классах наследниках мы можем переопределить базовый метод с помощью ключевого слова override
Добавим в класс Cat функцию
public override string ToString() { return Name + ", вес " + Mass + " кг., хозяин " + Master.Fio; }
В класс Person функцию
public override string ToString() { return Fio + ", вес " + Mass + " кг." ; }
И вновь посмотрим на вывод кода
foreach (Cargo currCargo in cargoList) { Console.WriteLine(currCargo); }
Важной особенностью наследования является то, что им не следует чрезмерно увлекаться. Все методики программирования должны упрощать код, а не усложнять его. Длинные цепочки наследования могут создать массу проблем. Частой болезнью начинающих программистов является то, что узнав о продвинутых и красивых методиках программирования они начинают пихать их повсюду, даже если это вредит проекту. Стив Макконел в книге Совершенный код не рекомендует создавать более 2-3 уровней наследования и более 7-9 наследников базового класса.
Другим правилом в отношении наследования является принцип подстановки Барбары Лисков. Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа не зная об этом. Или, если попроще, подклассы должны являться специализированной версией базового класса. В нашем случае и человек и кот являются специализированными версиями груза для корабля. Не стоит забывать, что членами класса могут быть любые другие классы. Пример плохого использования наследования (тоже с котом!) из Совершенного кода
Так же стоит с подозрением относиться к классам, которые переопределяют метод, делая его пустым. Например если есть базовый класс Cat, уже после создания которого выяснилось, что некоторые коты не могут царапаться и был создан подкласс ScratchlessCat. Но что делать, если вы найдете кота без хвоста? Кота, который не пьет молоко? Кота, который не ловит мышей? В итоге можно получить гигантскую иерархию с классами вроде ScratchlessTailessMicelessMilklessCat. На самом деле в данном случае вместо наследования надо использовать включение, добавив в базовый класс Cat класс Claws в качестве одного из членов.
3.5 Абстрактные классы и интерфейсы
Вполне логично предположить, что корабль не может перевозить сферические грузы в вакууме, у которых есть только вес. Соответственно экземпляры класса Cargo никогда не будут создаваться. Чтобы жестко зафиксировать это правило в коде мы можем сделать Cargo абстрактным классом.
public abstract class Cargo { private double _mass = 0; public double Mass { get { return _mass; } } public Cargo(double mass) { _mass = mass; } }
Теперь код вида
Cargo cargo = new Cargo(1);
Вызовет ошибку прямо в компиляторе.
В абстрактный класс можно включить описание абстрактных методов - они пустые, у них нет базовой реализации, но они обязательно должны быть реализованы во всех дочерних классах, пусть и по-разному. У любого груза должен быть хозяин. При этом хозяином человека будет он сам, по крайней мере пока не разрешат рабство, вряд ли в системе управления кораблем надо учитывать такую возможность. Добавим в уже абстрактный класс Cargo абстрактную функцию GetOwner();
public abstract class Cargo { private double _mass = 0; public double Mass { get { return _mass; } } public abstract Person GetOwner(); public Cargo(double mass) { _mass = mass; } }
И программа перестанет компиллироваться с говорящей ошибкой 'TestConsoleApplication.Person' does not implement inherited abstract member 'TestConsoleApplication.Cargo.GetOwner()' - так как теперь в каждый класс, наследующийся у Cargo должна быть добавлена такая функция
Как уже упоминалось выше в C# свойства вида
public double Mass { get { return _mass; } }
На самом деле представляют из себя функции со специальной формой записи, добавленной для удобства - чтобы не писать каждый раз две функции на Get и Set, так что абстрактным может быть и такое свойство
public abstract class Cargo { private double _mass = 0; public double Mass { get { return _mass; } } public abstract Person Owner { get; } public Cargo(double mass) { _mass = mass; } }
Так или иначе, его надо реализовать во всех наследниках абстрактного класса.
В Person
public override Person Owner { get { return this; } }
В Cat
public override Person Owner { get { return Master; } }
Для демонстрации изменений можно использовать такой цикл
foreach (Cargo currCargo in cargoList) { Console.WriteLine("Вес " + currCargo.Mass + ", владелец " + currCargo.Owner.Fio); }
Можно было было бы добавить новые типы грузов - багаж и контейнеры, какие-то методы для хозяина и корабля, но суть понятна и без этого.
Абстрактный класс может включать себя и абстрактные и обычные методы. Но реализующий эти методы класс может наследовать их только от одного абстрактного класса. Причина такого ограничения в том, что если мы наследуем методы и свойства сразу от нескольких классов, то получаем очень сильную зависимость от изменений в базовых классах, или классах, которые являются базовыми для наших базовых классов. Стив Макконел сравнивает множественное наследование с неисправной бензопилой, которая может внезапно заработать сама по себе. Но абстрактные методы не страдают от этого недостатка.
Интерфейс - это по сути дела абстрактный класс, в котором могут быть только абстрактные методы без данных на уровне класса. Класс может наследовать, а точнее говоря релизовывать много интерфейсов. По сути интерфейс описывает требование к классу иметь определенный метод или методы.
Можно зайти с другой стороны. Абстрактный класс - это интерфейс, включающий в себя уже реализованные методы и свойства-данные.
В рамках нашего несчастного примера, надуманность которого растет с каждой новой строчкой кода, можно добавить интерфейс IMovable для способных двигаться объектов с методом Move - перемещающим экземпляр в точку с заданными координатами.
public interface IMovable { void Move(int x, int y); } public class Cat : Cargo, IMovable { public override string ToString() { return Name + ", вес " + Mass + " кг., хозяин " + Master.Fio; } public string Name = ""; public void Move(int x, int y) { // реализация перемещения } public override Person Owner { get { return Master; } } public Person Master = null; public Cat(string name, Person master, double mass) : base(mass) { Name = name; Master = master; } }
Пример интерфейса из реальной жизни - IEquatable
Более глубокое раскрытие темы интерфейсов выходит за рамки руководства для начинающих. Как минимум вы теперь можете ответить на вопрос "Чем отличается абстрактный класс от интерфейса?", незнание ответа на которой часто приводят в качестве примера низкого уровня знаний у программистов.
3.6 Инкапсуляция и полиморфизм
Уже заканчивая наш запредельно краткий рассказ про ООП обнаружил, что так ни разу и не упомянул красивый и модный термин полиморфизм, хотя на самом деле он уже использовался в вышеприведенных примерах. Основными теоретическими принципами ООП считаются наследование, инкапсуляция и полиморфизм. Наследование рассмотрено чуть выше, инкапсуляция на самом деле применялась в предыдущей главе - так как под этим хитрым термином понимается то, что мы скрываем внутри класса подробности его работы, что бы не загружать ими голову при использовании уже готового класса. Наглядный пример:
public string FioInitials { get { string fio = Surname + " " + Name.Substring(0, 1) + ". " + Otchestvo.Substring(0, 1) + "."; return fio; } }
Свойство класс Person со свойством FioInitials инкапсулирует в себе детали составления ФИО. Кстати говоря, это свойство не доделано даже для примитивного тестового примера, так как в нем не учитывается возможность того, что имя или отчество будет неизвестно, в таком случае оно просто вылетит с ошибкой.
Полиморфизм, если отбросить в сторону хитрую теорию, означает, что в реальности встречается много однотипных задач, отличающихся друг от друга только мелкими подробностями - например надо выполнить одни и те же действия над разными данными или получить одни и те же данные разными способами. Таким образом мы получаем много похожих друг на друга, но не одинаковых действий. Чуть выше мы ставили перед собой задачу узнать владельца груза (например чтобы потребовать с него плату) - для кота и человека она решалась по-разному. Поэтому мы создали базовый класс Cargo и два подкласса, по-разному реализующих эту задачу. Это и есть полиморфизм, в нижеприведенном куске кода мы используем полиморфизм на уровне классов, так как в списке грузов используются две разных реализации груза, по-разному получающие данные о владельце, но для нашего кода эти две разные реализации выглядят одинаково при обращении через базовый класс.
foreach (Cargo currCargo in cargoList) { Console.WriteLine("Вес " + currCargo.Mass + ", владелец " + currCargo.Owner.Fio); }
Немного сложно и замороченно? На практике на порядок чаще используется полиморфизм на уровне функций. Если у нас есть функция, которая делает что-то одно, но при этом может принимать на вход разные наборы входных данных - то мы можем сделать несколько разных функций для выполнения одной и той же операции над разными наборами данных или создать несколько вариантов одной и той же функции, которые будут принимать разные наборы данных - это называется перегрузкой методов и в плане теории означает полиморфизм на уровне функций. Самым простым и наглядным примером может послужить функция вывода данных в окно консоли с последующим переходом на следующую строку, которая постоянно использовалась во всех трех статьях этой серии.
На самом деле у функции Console.WriteLine целых 19 перегруженных вариантов, которые принимают разные типы данных или даже разные наборы данных, преобразовывают их в строку по определенным правилам и выводят в консоль. WriteLine() - не принимает никаких данных на вход и выводит пустую строку, WriteLine(String) - выводит собственно строку, WriteLine(Object) - выводит в консоль результат работы функции ToString данного класса, WriteLine(String, Object, Object) - выводит информацию о нескольких обьектах, форматируя ее согласно данным из строки первого аргумента.
Таким образом, когда мы переопределили унаследованный от Object метод ToString для класса Person
public override string ToString() { return Fio + ", вес " + Mass + " кг." ; }
и Cat
public override string ToString() { return Name + ", вес " + Mass + " кг., хозяин " + Master.Fio; }
И потом использовали перегруженный вариант ToString в перегруженном варианте WriteLine через экземпляр базового для Person класса Cargo
foreach (Cargo currCargo in cargoList) { Console.WriteLine(currCargo); }
то мы на самом деле одновременно использовали и полиморфизм на уровне класса, и полиморфизм на уровне функций, и инкапсуляцию (подробностей о формировании строки с информацией внутри членов класса и их сочетания в методе ToString). На практике это гораздо проще, чем может показаться после злоупотребления умными терминами.
3.7 Библиотеки классов
Классы могут использоваться во множестве разных программ. Чтобы не усложнять их проекты файлами с исходным кодом, который так же может быть коммерческой тайной, используется механизм динамически подключаемых библиотек - проще говоря переносимые классы выносятся в отдельный файл с расширением .dll (dynamic-link library), который потом подключается к другой программе-проекту. Вынесем наших кошек и людей в отдельную библиотеку классов. Для этого нужно запустить еще одну копию Visual Studio и создать в ней проект с типом библиотека классов/class library. Назовем его TestClassLibrary.
В этом проекте не будет никакого Program.cs, так это просто набор классов для использования в других проектах.
Считается хорошей практикой оформлять каждый класс отдельным файлом, имя которого совпадает с именем класса. В принципе можно помещать несколько классов в один файл или даже один класс внутрь другого, но мы же старательные ученики и все хотим делать правильно и красиво, не так ли? Переименуем Class1, добавим еще классы и скопируем код из предыдущего проекта. Удалим классы из предыдущего проекта, так что там останется только класс Program
Построив проект мы получим в его папке /bin/Debug файл TestClassLibrary.dll
Теперь эту библиотеку классов нужно добавить в исходный проект, делается это в пунке References проекта.
И пропишем в самом начале
using TestClassLibrary;
Все, старый код работает точно так же, как если бы эти классы были в нем самом, хотя на самом деле они загружаются из dll библиотеки классов. При копировании exe файла нашей программы в самом простом случае надо копировать вместе с ним и все нестандартные dll. Можете попробовать скопировать в другую папку exe файл без dll и посмотреть на ошибку при запуске.
Как вы могли заметить по разделу References все стандартные классы точно так же подгружаются из dll библиотек - просто библиотеки вроде System.dll идут в комплекте с самим .Net Framework. Ничто не мешает вам скачивать из интернета или покупать чужие библиотеки.
3.8 Итоги
На самом деле подавляющее большинство программистов занимается именно тем, что описано в этих трех главах. Использует классы из разных библиотек, пишет свои классы, реализует разные алгоритмы с циклами и ветвлениями внутри функций. Многие программисты из реального мира, пишущие коммерческие программы за не менее реальные деньги, крайне редко сталкиваются даже с интерфейсами, не говоря уже о более продвинутых методиках, тем более что для их для осмысленного применения нередко требуется либо опыт разработки реальных задач либо руководство опытных товарищей в рамках конкретной задачи. В рамках самоучителя для начинающих дальнейшие главы имеют смысл только в привязке к какой-либо конкретной технологии. В наше время программисты на C# чаще всего:
- пишут настольные приложения на основе Windows Forms, эта морально устаревшая технология до сих пор используется в массе компаний, так как полностью удовлетворяет основным требованиям для специализированных офисных приложений
- настольные приложения с помощью технологии WPF, это более современная технология, позволяющая использовать более продвинутые методики программирования и более красивые интерфейсы с анимацией и прочими красивостями
- сайты/вебприложения с помощью устаревшей, но все еще используемой в деловом секторе технологии WebForms
- сайты с помощью технологии ASP.NET MVC
- служебные части приложений или отдельные библиотеки с помощью технологий вроде WCF
- мобильные приложения по Windows 8/Phone
Вне обширного царства Microsoft
- приложения для Android/iOS/MasOS с помощью Xamarin
- игры с помощью Unity 3D
- приложения для Linux с помощью Mono
Класс! Жаль забросил, а так то что надо!!
Большое спасибо, за то что автор написал простыми словами... Так намного всё понятнее... И еще можете добавить какие не будь языки программирования, языки разметки тот же HTML...
Просто лучший учебник по программированию в принципе!![:-(](http://nullpro.info/wp-includes/images/smilies/frownie.png)
Жаль, что не продолжения!
Спасибо. Продолжение придется писать по конкретной технологии - здесь уже начинаются сложности, и интересно не всем и я не все знаю. Но скорее всего будет в будущем.
Mickey Horton (Sarah and Mea7isl̵s;s father – Melissa was adopted), Bill Horton (Mike, Jenn and Lucas’ father), Marie (I don’t think she had any children, and was a nun last I knew), Tommy (who I think Nick Fallon was part of his descendants – grandchild or something?) and Addie (Julie’s and Hope’s mother and yes, the same Julie who is married to Hope’s father, Doug Williams; she is Hope’s half-sister) who was killed years ago.
Еще про делегаты интересно было бы прочитать в вашем изложении.
Интересно было бы почитать про ASP.NET, конечно же в Вашем авторстве. Что конкретно - не знаю, может простейшие примеры создания чего либо?![:)](http://nullpro.info/wp-includes/images/smilies/simple-smile.png)
Спасибо за уроки.
Вы можете побольше описать про пункт Интерфейс.
public void Move(int x, int y)
{
// реализация перемещения
}
А пример реализации перемещения можно ?
Ну это не столь ж важно. Все зависит от контекста решаемой задачи. Конкретно в этом случае в классе могут храниться координаты объекта, а данный переопределенный метод меняет их на те, которые подаются как аргументы
Статья рассчитана на начинающих программистов, а в одной главе пытаются объяснить достаточно серьезные ооп парадигмы. Все чересчур сумбурно и поверхностно. Имхо если уж и объяснять про наследование и полиморфизм, то следует это делать сразу в полном объеме.
Давно пишу на другом языке. Надо быстро кое что мелкое написать на C#. Кратенькая инструкция с описанием базовых особенностей и основных нюансов. Просто, быстро и понятно. Стиль изложения понравился
Большое спасибо!
Пушкин весил 72 кг., так на будущее.