Этой строчкой вы
включаете функцию "нажалиКнопку" в
список рассылки информации о событии
"Click" (нажатие) для кнопки "кнопка1". Помнить
надо только об одном - любые владельцы
рассылок люди хитрые, конкурентов не
любят и рассылают только "своим", что в
переводе на программирование значит
- функция, которую вы вносите в список
должна быть именно такого вида, какого
ждут в списке, иначе не примут. Последний момент -
EventHandler - это посредник между
списком рассылки и получателем, именно
он держит имя функции, которой надо
сообщить о событии. EventHandler'ов
столько же, сколько и разных event'ов, и он тоже должен
быть именно того типа, который ждут.
Конечно код бывает
разный, и чтобы читать чужой код, который
написан на слабознакомом языке, без
комментариев и в непривычной манере
нужно не только умение, но и изрядная доля
удачи и интуиции. Однако если код
довольно простой и написан с
комментариями, хотя бы с какими-то, то
прочитать его проблем обычно не составляет.
Большой плюс C# в этом отношении, это возможность
вбить непонятную строчку в Google или MSDN и
почти наверняка найдется пример с
подробным описанием, или что-то подобное.
Больше чем про C#, среди .NET языков, в сети только про
VB написано.
Итак, разбор кода.
Рассмотрим код внутри функции, так как все
остальное обычно вопросов не
вызывает.
Код, как привило,
структурирован - или форматирован
табуляцией, или как-то иначе (например
region'ами). Очень часто каждый блок имеет
комментарий, описывающий
производимую внутри операцию -
например, "Чтение файла", "Заполнение
таблицы", "Подбор размеров". Таким
образом, мы знает что там происходит,
осталось понять как. Большинство, т.е.
почти все, функции и свойства
стандартных классов обладают
названиями точно описывающими их
действия. Если вы не знаете английский -
попробуйте сначала перевести
название функции, разбив его на отдельные
слова по большим буквам:
GetChildFromPoint = Get Child From Point
= Получить Ребенка Из Точки - получить
дочерний элемент управления,
находящийся в точке. Другой вариант -
переводить имя класса и функцию:
Convert.ToString = Convert To String =
Перевести В Строку. Некоторые
термины, конечно, придется поискать в
сети и после перевода. А некоторые
перевести понятно не получится,
поэтому придется понять, что именно
функция делает. Например, в такой вот
строчке:
даже если вы понятия
не имеете что такое Enum, можно многое понять из
простого перевода: объект ComboBox
(выпадающее окно), его свойство Источник
Данных (DataSource)
приравнивается чему-то
непонятному. Логично предположить,
что источник данных указывает на
какого-либо вида список значений.
Функция, предположим
неизвестного, класса Enum
называется GetNames - получить
имена, возвращает она массив строк,
который и заполнит выпадающее окно
значениями.
Подведем итоги -
ничего толком полезного я не сказал. Все мои
советы можно свести к трем фразам:
1. внимательно
читайте код
2. учите
английский, кто не знает
3. пользуйтесь
справкой (MSDN) и Google'ом.
Вторая попытка
написать что-то полезное для совсем
начинающих в С#.
Чуть-чуть о структуре
кода: верхним элементом структуры
являются пространства имен
(namespace), довольно фиктивная вешь,
призванная упорядочить ту кучу классов,
списков, интерфейсов и констант, которые
уже существуют и будут создаваться. В
namespace могут входить другие
namespace, классы, enum, константы и
интерфейсы. Класс состоит из функций,
переменных и констант. По поводу
переменных - они могут быть объявлены на
любом уровне, и существовать будут
только в пределах (и во время жизни) того
блока, в котором объявлены. Например:
переменная объявленная в классе
доступна для всего класса, часто для других
классов, живет все время, пока жив класс (если он
статичный) или объект класса (если не
статичный). Другой пример: переменная
объявленная в пределах блока if { },
который находится внутри блока for { },
который находится внутри функции
класса... такая переменная будет
доступна только в пределах блока if {}, в
котором объявлена и будет жить пока не
закончится выполнение блока, т.е. даже
до конца функции не доживет.
О том, что такое
статический класс, что значит переменная
объявлена и пр - читайте ниже.
Декларации - или
объявления - это определение имени и
формирование типа, прикрепляемого к
этому имени. Т.е. если вам нужно место для
хранения целого числа, вы должны
объявить переменную, с понятным вам именем,
типа данных целое (int). К слову, int = integer =
целое число. И помните, декларация не
создает объект и не выделяет память - это
только закрепление имени за типом. Для
создания декларированного
объекта необходимо его
инициализировать (или определить), т.е.
либо присвоить начальное значение, либо
запустить конструктор.
Декларация классов,
функций и переменных на уровне класса
требует указания модификатора
доступа. Впрочем, если не указывать будет
использован стандартный, обычно
private. Модификаторы доступа - это
ключевые слова, определяющие,
откуда можно будет получить доступ к
декларированной
переменной/функции и пр. Их всего
несколько:
Есть еще
модификаторы состояния,
определяющие принципиальное
состояние функций:
Модификатор доступа для
функции/переменной внутри класса не
может быть более доступным, нежели
модификатор доступа всего класса.
Переменные,
декларируемые внутри функций
модификатора доступа не
требуют.
Декларация переменных
обязательно состоит из типа данных и
имени, остальное - по ситуации и
желанию.
Декларация функций
обязательно состоит из типа
возвращаемых данных, имени и списка
аргументов, остальное - по ситуации.
Возращаемым типом данных может быть void -
пустой - т.е. нет возвращаемых данных. Список
аргументов тоже может быть пустым.
Функции, в отличии от переменных, не
могут быть только декларированы - они
обязательно сразу определяются.
Единственное исключение - тип
функций abstract, они только
декларируются, а определяются
уже в классах наследниках.
Декларация класса может
включать указание класса родителя, через
двоеточие, далее через запятую -
подключенные интерфейсы.
Интерфейс - список функций. В каком-то
кривом смысле - родительский класс,
который можно произвольно прилепить
куда угодно. Таким образом
получаются классы, от разных
родителей, но поддерживающие одни и
те же операции. Довольно удобная вещь.
Декларация
конструктора класса может включать
указание на другой конструктор,
который должен быть запущен до
декларируемого, например,
конструктор родительского
класса:
И последнее -
декларация Enum. Enum = enumeration =
перечисление. Список значений.
Создается для двух целей: его можно
использовать программисту - связать
список целочисленных значений с
понятными словами, и пользоваться
словами, а не числами, в которых легко
запутаться; его можно использовать
пользователям - имена значений в списке
можно легко вывести на
пользователя.
Глава 3. Синтаксис
деклараций массивов, свойств, делегатов.
Assembly.
Массивы
Итак, массив - набор
объектов одного типа, имеющий четкую
последовательность этих объектов и
строго заданный размер, т.е. количество
объектов в наборе. Массив может быть
любого типа, размер массива должен быть
больше 0 и меньше чем максимальное значение
int (на 32 разрядных машинах - 2^32=4294967296).
Декларация массива
подобна декларации переменной, с
одним только изменением - после типа
декларируемого массива
добавляются квадратные скобки - [].
Пример:
int[]
intArray;
В строчке выше
показана декларация
одномерного массива типа int. Массив
может быть многомерным. Причем в .NET
многомерность может быть разной.
Вариант 1 -
многомерный массив старого образца, так
называемый массив-массивов:
int[][]
int2dArray;
Система простая - каждая
скобка создает свой массив. Т.е. можно
представить это в таком виде: (int[])[]
arrayName - массив типа int[], который
тоже является массивом. Подобных
вложений может быть несколько.
Вариант 2 -
многомерный массив .NET:
int[,]
int2dArray;
Внутри скобки ставится
нужное количество запятых, каждая
запятая создает плюс одно измерение
массива. Т.е. в приведенной выше строчке
декларируется таблица (двумерный
массив) для целочисленных данных.
У каждого
варианта есть свои плюсы и свои минусы. Если
коротко - второй вариант удобнее и чуть
быстрее работает, зато первый
вариант позволяет разделять массив на
составляющие при необходимости.
Примеры использования массивов
будут потом.
Свойства
Свойства - специальная
фича для возможности проверки данных,
вводимых в переменную и задания
каких-либо действий при изменении
значения. Своего рода посредник между
переменной и прочим кодом, а по
совместительству - охранник и
секретарь этой переменной :). Свойства
существуют только внутри класса, вместе
с переменными уровня класса.
Задаются очень просто:
private float _size;
//переменная
public property float
Size { //свойство - посредник переменной
_size
get {
return _size;
}
set {
//проверить на
правильность
_size = value;
OnSizeChanged();
}
}
Сначала, как обычно,
модификатор доступа. Ключевое слово
property указывает, что
декларируется свойство. Далее
указывается тип данных, в примере -
одинарное дробное. Затем имя свойства и
открывается фигурная скобка,
начинающая блок описания. Любое свойство
должно иметь блок get - получить. Если
свойство только-для-чтения, оно не имеет блока set
- установить. В каждом блоке
прописывается, что надо сделать.
Помните, что свойство не хранит данные, т.к. не
является переменной - оно лишь
посредник, поэтому в блоке get указано -
вернуть значение переменной _size, в
которой значение хранится реально. В
блоке set написано, что делать при
установке нового значения - сначала
проверить на правильность... ну, например,
чтобы вводили от 0 до 1, когда нужно и т.п. После
проверки - установить значение;
ключевое слово value, в данном случае,
указывает на входящее значение. Ну и под
занавес - выполнить событие "размер
изменился". Завершает декларацию
закрывающаяся фигурная скобка.
Свойства позволяют
создавать все переменные как private, а
нужные для открытого доступа
выводить через свойства, имея таким
образом гарантию, что любое внешнее
изменение значения будет проверено
на правильность.
Делегаты
Делегаты - если просто, то
это указатели на функции. Если вам надо
передать функцию как аргумент, то у вас два
пути - либо через строковое название
функции искать ее в библиотеке (assembly) и
вызывать как внешнюю функцию, либо
использовать делегат. Делегат
позволяет определить подпись
функции - т.е. возвращаемый тип, типы и
количество аргументов.
Делегат - это объект уровня
класса, собственно это и есть класс, только очень
особый. Однако декларация его очень
похожа на декларацию
абстрактной функции:
public delegate
double MyDelegate(double d1, double
d2);
Не забывайте, что
делегат - это класс, и для того, чтобы его можно
было использовать, необходимо
создать объект делегата, который
приписывается к конкретной
функции и дальше передается, если
нужен. У делегата есть метод Invoke,
который собственно и запускает
функцию, на которую делегат
указывает.
Пример
использования делегата внутри
функции:
MyDelegate md = new
MyDelegate(MyFunction);
md.Invoke(d1,
d2);
В примере
предполагается, что d1 и d2 - переменные
типа double, a функция MyFunction
имеет подпись: double
MyFunction(double d1, double d2).
Assembly
Кратко: Assembly = сборка,
файл, являющийся контейнером для
namespace/классов/констант/делегатов/интерфейсов
и ресурсов (картинок/текста/иконок и пр.).
Assembly может быть запускным (exe) или не
запускным (dll). Отличие только в том, что в
запускном файле прописан метод
WinMain (или main для консольных
приложений). Несмотря на расширения
файла, ничего общего (кроме
пользовательского применения) со
старыми (до .NET) файлами не имеет. Основные
два отличия от win32 стандартов - assembly имеет
собственное описание внутри себя.
Описание включает: версию, автора,
параметры безопасности
необходимые для выполнения и многое
другое, в частности - описание подписей
всех функций, переменных и т.д. Т.е. не надо
знать заранее как вызывать ту или иную функцию из
библиотеки - это можно узнать на лету. Второе
отличие - assembly хранит код не в виде
ассемблера, а в MSIL - Microsoft
Intermediate Language -
своеобразный мета-язык, который
компилируется на лету и по запросу
(just-in-time compiling and debugging) в
соответствии с требованиями
системы на которой компилируется
(разрядность процессора и т.д.). Плюсы
такого подхода - кросс-платформенность и
кросс-системность, а также безопасность
выполнения. Минусы - код всегда открыт и
доступен любому для прочтения, а также
уходит время и ресурсы на компиляцию.
Впрочем, книги и музыку тоже может любой
прочитать/услышать... и почему-то авторов это
не напрягает, а плагиаторов не так уж
много...
Assembly может быть
одномодульным и многомодульным.
Модуль - часть assembly, как правило, до сборки
представляющая собой отдельный файл. Т.е.
несколько библиотек классов можно
объединить в одну assembly, и добавить еще
картинки до кучи. Каждая картинка и каждая
библиотека будут отдельным модулем
внутри единой assembly. Плюсов в таком
подходе маловато, поэтому редко
используется. MS Visual Studio даже не
имеет возможности создавать
мультимодульные assembly в интерфейсном
режиме.
Лично я отношусь к
assembly по старому - как к exe и dll win32 стандарта.
Единственное, о чем приходится помнить -
assembly должны иметь идентификаторы,
если ими собираетесь пользоваться не вы один.
Т.е. версия, автор и разрешения
безопасности должны быть прописаны.
Да и подписать assembly ключом strong name тоже
нужно. Ключ strong name - уникальный
идентификатор assembly, который
используется для распознавания
разных assembly с одним именем на одной
машине. Для подписывания есть консольная
утилита sn.exe, а в VS2005 есть и интерфейсные
опции в свойствах проекта для этой цели.
Глава 4. Основные операторы и
конструкты.
Рассмотрев синтаксис
деклараций, переходим к основным
конструктам или командам языка. Как
известно, код внутри функции
выполняется построчно, если не
используются какие-либо команды.
Другими словами - команды языка
призваны обеспечить нелинейность
выполнения кода. Самая известная из
не-линейных команд - goto - есть в языке C#,
однако я настоятельно не рекомендую ей
пользоваться. Остальных команд вполне
достаточно, чтобы обеспечить любую
нелинейность, а привыкать к goto - это как
правило означает привычку к плохой
структуризации кода.
Но сначала, чтобы
было проще понимать команды, рассмотрим
некоторые операторы языка.
Операторы
Операторы сравнения:
‹ - строго меньше.
Определен для любых числовых типов (int,
double, short, byte, decimal, float, bool)
› - строго больше.
Аналогично предыдущему.
‹= - меньше или равно.
Аналогично предыдущему.
›= - больше или равно.
Аналогично предыдущему.)
== - равно
(эквивалентно). Определен для
большинства типов и классов. Для многих
классов означает именно идентичность
объектов класса, а не равенство их
внутренних значений.
!= - не равно.
Аналогично предыдущему.
Обратите внимание:
проверка на равенство
обозначается двумя знаками равно!
Одним знаком обозначается оператор
присвоения значения.
Логические
операторы:
|| - логическое
ИЛИ.
&& - логическое И.
! - логическое
НЕ.
Арифметические
операторы:
помимо стандартных +,
-, *, / есть их модификации со знаком равно: +=,
-=, *=, /=. Означают "выполнить оператор и
приравнять", т.е.
i += 10;
означает прибавить 10 к
значению переменной i, и записать
результат в нее же. Это аналогично
записи:
i = i + 10;
есть еще два
оператора:
++ - прибавить
единицу к переменной и записать
результат в нее. Инкрементный
оператор.
-- - отнять единицу от
значения переменной и записать
результат в нее. Декрементный
оператор.
Основные
конструкты можно разделить на
цикловые - позволяющие запускать кусок
кода в цикле, и условные - позволяющие
выполнять нужный кусок кода, выбираемый
по условию. Я опишу основные
конструкты, если какой забуду, а вам он
интересен - пишите.
Циклы
Самый
распространенный цикл - for. Смысловое
предназначение - выполнять
последовательность действий
заданное количество раз. Задается
следующим образом:
for (i = 0; i ‹ 10; i++) { /*Ваш код
здесь*/ }
После ключевого
слова for следует круглая скобка, внутри
кторой обязательно присутствуют три
выражения, разделенные точкой с
запятой. Первое выражение, в нашем
случае "i=0" - операция, которую надо
выполнить перед началом цикла. Зачастую
в этом месте пишут декларацию
переменной, которая существует
только для цикла, тогда выражение
выглядит, например так: "int i = 0". С тем же
успехом поле может быть пустым. Второе
выражение устанавливает
условие, при котором цикл должен
продолжаться... его можно рассматривать как
"Продолжать цикл пока выражение 2
истинно". Может быть пустым, но тогда вам самим
придется внутри цикла проверять условие
выхода и самим же выходить из цикла. Третье
выражение задает операцию, которую
надо выполнить по завершении каждого
круга цикла. Тоже может быть пустым.
Итак в строчке примера
показан цикл, обнуляющий переменную i
вначале, исполняющийся пока
переменная i меньше 10, после каждого
круга увеличивающий i на 1, что дает 10
выполнений цикла, при нормальных
условиях.
Для
дополнительного управления
выполнением цикла предусмотрены еще
два ключевых слова: continue - завершает
текущий круг выполнения, break -
завершает цикл.
for (int i = 0; i ‹ 10; i ++) { //
цикл на 10 кругов
if (some_array[i] == 0) {
continue;} // если i-тая ячейка массива равна
0 - пойти на след круг
int res =
DoSomething(some_array[i]); // что-то сделать с
ячейкой массива и вернуть значение
if (res == -1) { break;} //
если вернулось -1 - прервать цикл
}
Цикл while (бывший
do...while). Смысловое предназначение -
выполнять последовательность
действий, пока что-то не
случится/изменится.
задается так:
while (flag == true) { /*Ваш код
здесь*/ }
После ключевого
слова while следует круглая скобка, в
которой задается логическое
выражение, проверяемое на истинность
каждый круг цикла. Цикл выполняется пока
выражение истинно, или до ключевого
слова break, внутри цикла. Слово continue
применимо к этому циклу так же, как и к
предыдущему.
Цикл foreach.
Смысловое предназначение - выполнить
кусок кода для каждого члена
массива/списка/коллекции. В .NET появились
классы обеспечивающие
ненумерованные, динамические
массивы. В основном для них и был сделан этот
оператор.
задается так:
foreach (Class
objectName in collectionName) { /*
Ваш код здесь */ }
После ключевого
слова foreach следует круглая скобка в
которой задается класс одиночного
объекта, над которым выполняется
операция в основном блоке, имя объекта
(переменной) для обращения в
основном блоке, затем ключевое слово in и имя
переменной, указывающее на
массив/список/коллекцию объектов того
класса, который указали в начале
условия. Основной код, как обычно, пишется в
фигурных скобках.
Ну, например, часто
встречается - цикл по всем файлам какого-либо
типа в каталоге:
DirectoryInfo di = new
DirectoryInfo("C:\\temp"); // создать объект
информации о каталоге С:\temp
foreach (FileInfo fi
in di.GetFiles("*.txt")) { // для каждого объекта
типа FileInfo в коллекции,
возвращаемой функцией
GetFiles
// что-то сделать
}
Условия
Условие if...else.
Смысловое предназначение - выполнить
блок кода, только если условие истинно... с
вариантом - если ложно - выполнить
другой кусок кода.
задается так:
if (flag == true) { /* Ваш код здесь
*/ }
else if (flag2 == true) { /* Ваш код
здесь */ }
else { /* Ваш код здесь */
}
После ключевого
слова if следует логическое условие в
круглых скобках, при истинности которого
выполняется код, расположенный в
фигурных скобках. Если условие ложно -
выполняется следующий блок. Следующий
блок должен начинаться с ключевого слова
else, за которым может следовать слово if и
еще одно условие. Такая цепочка может быть
длинной, но надо помнить, что после
выполнения хотя бы одного блока
цепочка прерывается.
Пример:
bool flag1 = true, flag2 =
false;
if (flag1 && flag2)
{DoSomething(); }
else if (flag1) {
DoSomething2(); }
else { DoSomething3();
}
В этом пример будет
выполнена только функция
DoSomething2(). В первом условии мы
проверяем оба флага на истинность, если
хотя бы один не равен истинно, переходим на
второй блок. Второй блок тоже с условием -
проверяем первый флаг на истинность - мы
точно знаем, что хотя бы один флаг ложен, если
первый истиннен - второй ложен. Третий блок -
если первый не истиннен, то либо второй
истиннен, либо оба ложны, но возможно нам это уже
не важно...
Пример 2:
int i = 1;
if (i == 0) {
DoSomething1(); }
else if (i == 1) {
DoSomething2(); }
else if (i == 2)
{DoSomething3(); }
else { DoElse();
}
В этом, крайне
неграмотном примере, тоже будет
выполнена только функция
DoSomething2(). А грамотно такую
ситуацию расписывать через оператор
switch;
Условие switch...case.
Смысловое предназначение - выполнять
куски кода по значению переменной.
Своеобразная развилка (или
переключатель). С дополнительным
возможностями.
задется так:
switch (var) {
case ‹val1›:
DoSomething();
case ‹val2›:
DoSomething2();
break;
default:
DefaultAction();
break;
}
После ключевого
слова switch следует имя переменной, по чьему
значению нужно разветвить код. В фигурных
скобках задаются варианты
значений, которые должны
обрабатываться. Вариант значения
задается как ключевое слово case,
значение переменной, двоеточие.
Если функциональный блок
заканчивается словом break -
программа выходит из блока switch после него,
если не заканчивается - программа
переходит на выполнение следующего
по порядку блока case. ключевое слово
default на месте case означает "все
варианты".
Пример:
int i = 1;
switch (i) {
case 2:
DoSomething2();
break;
case 1:
DoSomething1();
case 3:
DoSomething3();
break;
default:
DoDefault();
break;
}
В этом примере будут
выполнены функции DoSomething1() и
DoSomething3().
Переменная может быть не
числовой:
char c = '!';
switch (c) {
case '?':
DoQuestion();
break;
case '!':
DoExclamation();
break;
}
В этом примере будет
выполнена только функция
DoExclamation(), а если
переменная с будет равна не
вопросительному знаку и не
восклицательному - ничего
сделано не будет.
По поводу
нахождения основного кода внутри
фигурных скобок - если код состоит из одной
строчки, можно писать его без фигурных
скобок:
if (flag == true)
return;
Глава 5.
Инициализация переменных.
Коллекции. Типы данных.
Инициализация.
Я уже упоминал это
слово, в таком примерно контексте -
После декларации переменной, перед
ее использованием, необходимо эту
переменную инциализировать.
Попробуем разобраться точнее:
инициализация - процесс выделения
памяти под переменную и заполнение этой
памяти значениями по умолчанию.
Значения по умолчанию - это, почти всегда,
разные формы нуля: для int - 0, для double - 0.0, для
string - "", для bool - false и т. д.
Та часть, которая
касается выделения памяти, это
теперь дело компилятора и к
программисту почти не имеет никакого
отношения. С точки зрения программиста,
инициализация - первое присвоение
значения переменной.
Есть такая тонкость -
переменные, объявленные на уровне класса
инициализируются
автоматически при создании объекта
класса. Но это относится только к основным
типам данных (int, double, bool, byte и т. д.)
кроме string. Все переменные классов
получают при автоматической
инициализации значение null и string
тоже.
Комментарий для
специалистов: если быть точным, то null - это
значение по умолчанию для всех переменных
классов MarshalByReference -
обрабатываемых через указатель.
Основные типы данных являются
классами MarshalByValue -
обрабатываемых через значение,
поэтому им присваиваются нормальные
значения. Класс string по прежнему является
массивом, хоть и очень хитрым, и
обрабатывается
соответственно.
Инициализация бывает двух
типов:
для простых типов
данных это простое присвоение значения:
int i = 0;
double d = 0.2;
string s = "test
string"
Все это варианты
декларации с одновременной
инициализацией для простых типов
данных.
Второй вариант -
инициализация объекта класса:
DateTime dt = new
DateTime();
MyClass mc = new MyClass();
int[] intArray = new
int[20];
Обратите внимание -
массивы рассматриваются как
классы.
Для специалистов:
массив, как известно,
обрабатывается по указателю. А
простые типы данных тоже могут быть
инициализированы как классы: int i = new
int(); вполне правильная запись. Более того,
запись "int i = 2;" для компилятора равна "int i =
new int(); i = 2;".
Инициализировать
переменную можно почти в любой момент -
главное, до первого обращения. Такая
запись вполне допустима:
public partial class Form1
: Form
{
public DateTime dt =
new DateTime();
И последнее: ошибка
при компиляции "Use of unassigned local
variable ..." значит, что вы забыли
инициализировать переменную,
декларированную внутри функции, а
ошибка "Null reference exception ..." при
работе программы означает, что вы
забыли создать объект класса для
переменной, декларированной на
уровне класса.
Коллекции/списки и
массивы
Что такое массивы мы
уже рассмотрели, теперь рассмотрим такую вещь
как коллекцию/список и сравним.
Список - это любой класс,
который поддерживает интерфейс
IList, позволяющий создавать
динамические массивы.
Динамический массив - набор объектов
одного класса, с изменяющимся
количеством и порядком объектов по
ходу выполнения программы. Базовый
интерфейс IList содержит функции для
добавления нового объекта к списку,
убирания объекта из списка, вставки
объекта в список, поиска объекта и
очистки списка.
Коллекция - любой класс
порожденный от класса
CollectionBase,
обеспечивающего функции
поддержки списка. Класс
CollectionBase поддерживает
интерфейс IList.
Есть модификации
этих двух классов для создания коллекций
только-для-чтения, разнотиповых
коллекций, парных массивов (хешей) и
пр.
Преимущества списков -
динамичность выделяемой памяти.
Преимущества массивов -
скорость.
Как пользоваться -
если очень не лень, или очень надо - можете всегда
создать свой класс. Такой вариант мы рассмотрим
позже, когда будем говорить о
наследовании. Проще всего
пользоваться классом ArrayList:
ArrayList al = new
ArrayList();
int i = 2;
double d = 4.5;
string s = "test";
MyClass mc = new MyClass();
al.Add(i);
al.Add(d);
al.Add(s);
al.Add(mc);
Таким путем мы
добавили самые разные элементы в список
и можем до любого из них достучаться как до
элемента массива:
string s2 =
(string)al[2];
s2 будет равно "test". Есть
только одно неудобство - ArrayList
всегда возвращает помещенный в него
объект как object, поэтому для нормальной
работы нужно преобразовывать
возвращаемое значение в нужный тип... что не
всегда просто. Именно это преобразование
и совершает слово string, стоящее в круглых
скобках перед обращением al[2].
Подробнее об этом - чуть ниже.
Типы данных
У каждой
переменной есть свой тип данных или просто тип, то же
можно сказать о возвращаемом любой
функцией значении (void тоже тип, хотя и
особый). В .NET все типы данных являются
дочерними от Object, у которого есть
всего 4 функции, зато 2 из них нужны очень
часто:
Object.ToString() -
возвращает строковое
представление об объекте (для чисел -
строку с числом, для сложных объектов - некую
строку, описывающую главную информацию
объекта; например FileInfo - класс
информации о файле - в методе ToString()
возвращает полный путь файла.)
Object.GetType() -
возвращает объект класса Type,
описывающий тип данных, которому
принадлежит исследуемый объект.
Для сравнения типов
есть специальный оператор 'is'.
Используется так:
if (var is int) { do
something }
вместо var - имя
переменной, вместо int - тип данных (имя
класса) с которым вы хотите сравнить.
Рассмотрим пример из
предыдущего параграфа - у нас есть
ArrayList из 4 объектов, 4 разных типов:
for (int i = 0; i ‹ al.Length; i ++)
{
if (al[i] is int) { /*сделать
что-то с целым числом */ (int)al[i]}
else if (al[i] is double} {
/*сделать что-то с дробным числом*/
(double)al[i]}
else if (al[i] is string} {
/*сделать что-то со строкой*/ (string)al[i]}
else {
MessageBox.Show(al[i].ToString()); } //
показать сообщение со строковым
представлением объекта
}
В этом примере мы
пробегаем циклом по списку, смотрим на тип
очередной переменной, если это что-то
простое - преобразуем и что-то делаем,
если нет - показываем в сообщении
строковое представление объекта.
Теперь о
преобразовании.
Преобразованием типов данных
называют два разных процесса.
Один - это конвертация
(Convert) - настоящая смена типа. Например,
превратить double в int - т.е. отбросить
дробную часть, или округлить до ближайшего
целого. Или, еще например, превратить
double в string - т.е. создать строку, где
записано число, хранимое в
переменной.
Второй - это смена типа
(cast) - без замены данных. Переменная типа
object может на самом деле хранить любой
объект, и чтобы компилятор понял, что от него
хотят - надо преобразовывать.
Бывают и более сложные вещи.
Конвертация
совершается всегда через функцию,
которая знает, как именно надо
преобразовывать типы. А все, что
касается строк - надо знать еще и настройки
культуры для которой ведется
преобразование. Есть специальный класс
Convert, который ведает
конвертацией. Им и рекомендуется
пользоваться.
double d = 4.2;
string s1 =
d.ToString();
string s2 =
Convert.ToString(4.2);
s1 и s2 -
одинаковы.
Преобразование может быть:
встроенным (implicit - неявным),
надстроенным (explicit - явным) и еще
одним :)... назовем его условным.
Встроенное - оно либо есть,
либо нет. Обеспечивается создателем
класса. Т.е. например int в double
преобразуется легко, самим
компилятором, а вот наоборот - только
человеком.
Надстроенное - это то, что
пишется в скобках перед именем
переменной. Например, можно вот так вот
сделать:
CheckBox cb = new
CheckBox();
object obj = cb;
((CheckBox)obj).Checked =
true;
Условное работает
только с объектами классов и не работает
с простыми типами данных, кроме string.
Выглядит оно как оператор 'as' - 'как'
(рассматривать как).
string s = al[2] as
string;
Оператор говорит
компилятору, что подсовываемую ему
переменную надо рассматривать "как"
указанный тип данных.
Глава 6. Пример
кода.
Итак, для первой части, я
думаю достаточно. С тем, что уже было
рассмотрено, можно начинать
программировать, а знающие другой язык
должны были уже получить
представление о C#. Остались
нерассмотренными еще многие вещи,
включая некоторые из основных, но в силу
того, что они несколько сложнее - я рассмотрю их в
следующих главах.
Пример кода:
namespace tst_form2
//задаем namespace
{
public delegate
double Delegate1(); // в нем
декларируем делегата
public abstract class
MyParentClass // создаем класс,
абстрактный
: Object,
tst_form3.IInterface1 // порожденный от
класса Object с подключенным
интерфейсом tst_form3.IInterface1
{
private int int1; //
внутреннее поле, доступное только внутри
класса
protected int Int1 { //
свойство для этого поля, доступное
наследникам
get { return int1; } //
возвратный код
set { //установочный
код
if (value › 0) { //
проверка на значение
int1 = value; // если
прошли - установить значение
}
else throw new
ArgumentException("Value must be › 0",
"Int1"); // если нет - кинуть ошибку
}
}
public double d1; //
открытые поля
public double d2; //
public
MyParentClass() { //основной
конструктор класса
int1 = 0; //
инициализируем переменную
начальным значением
}
public
MyParentClass(double inD1, double inD2)
// дополнительный конструктор
класса
: this() { // который
сначала запускает основной
конструктор и только потом
исполняется
d1 = inD1; //
инициализируем переменные
входящими аргументами
d2 = inD2; // если
аргументов нет - переменные
инициализируются сами,
стандартными значениями
}
protected abstract
double GetSum(); // шаблон функции для
инициализации в дочерних классах
#region IInterface1
Members // функции интерфейса
public abstract
double GetSubstract(); // функция
интерфейса - тоже только шаблон
#endregion
}
namespace tst_form3 //
объявляем еще namespace внутри
tst_form2
{
public class
MyChildClass //создаем класс
:
tst_form2.MyParentClass //порожденный от
tst_form2.MyParentClass
{
private Delegate1
getSumDel; // декларируем объект
делегата
internal
MyChildClass() //основной
конструктор
: base() { //
запускающий родительский и только
потом исполняющийся
getSumDel = new
Delegate1(GetSum); //
инициализируем объект делегата - на
функцию GetSum
}
internal
MyChildClass(double inD1, double inD2)
//дополнительный конструктор
: base(inD1, inD2) { //
запускающий дополнительный
родительский конструктор
getSumDel = new
Delegate1(GetSum); //
инициализирующий объект
делегата
}
#region inherited
members // унаследованные функции
protected override
double GetSum() { // определяем
родительскую абстрактную
функцию
return d1 + d2;
//возвращаем сумму
}
public override
double GetSubstract() { //
определяем родительскую
абструктную функцию
return d1 - d2; //
возвращаем разность
}
#endregion
internal void
ChangeInt1(int inInt) { // создаем функцию для
смены значения
Int1 = inInt;
}
public double
TryToGetSum() { //функция получения
суммы
return
StaticFunc.GetSumFromChildClass(getSumDel);
// вызываем статичную функцию
другого класса с делегатом для GetSum
}
}
public interface
IInterface1 //интерфейс
{
double
GetSubstract(); // функции
интерфейсов объявляются без
модификаторов
}
}
internal class StaticFunc
//класс для нашей статичной функции
{
public static double
GetSumFromChildClass(Delegate1
del) {
return del.Invoke(); //
возвращаем результат вызова
функции, приписанной к делегату
}
public enum
SupportedLanguages //простой Enum
{
Русский = 1,
English = 10,
German = 11
}
public static object[]
objArray = {1, 2.4, "string",
SupportedLanguages.Русский }; //
статичный массив
}
}//закрываем namespace
tst_form2
Для тестрования -
напишем еще такую функцию в любом другом
namespace... хоть в основном, созданном
автоматически при создании
проекта:
private void
button1_Click(object sender, EventArgs e) { //при
нажатии на кнопку
tst_form2.tst_form3.MyChildClass mcc
= new tst_form2.tst_form3.MyChildClass(1.2, 2.4); //
создаем объект нашего класса с
введенными значениями
double tmp_double =
mcc.TryToGetSum(); //вернет 3.6
tmp_double =
mcc.GetSubstract(); //вернет -1.2
/*mcc.ChangeInt1(-2);
//произойдет ошибка - неверный
параметр*/
mcc.ChangeInt1(2); //int1 станет
равен 2
int i1 =
(int)tst_form2.StaticFunc.SupportedLanguages.English;
// i1 = 10
string s =
tst_form2.StaticFunc.SupportedLanguages.English.ToString();//s="English"
s = "";
for (int i = 0; i ‹
tst_form2.StaticFunc.objArray.Length; i++) {
s +=
tst_form2.StaticFunc.objArray[i].ToString() +
"\t";
if
(tst_form2.StaticFunc.objArray[i] is int) s += "\n";
else {
switch
(tst_form2.StaticFunc.objArray[i].GetType().ToString())
{
case "System.Double":
s +=
Math.Round((double)tst_form2.StaticFunc.objArray[i]).ToString()
+ "\n";
break;
case
"tst_form2.StaticFunc+SupportedLanguages":
s +=
((int)(tst_form2.StaticFunc.SupportedLanguages)tst_form2.StaticFunc.objArray[i]).ToString();
break;
default:
s += "\n";
break;
}
}
} // цикл
завершается s = "1
//2.4 2
//string
//Русский 1"
}
Немного дополнений и
комментариев:
Если у функции есть
модификатор override - ее можно
переопределять во всех дочерних классах,
любого поколения.
Если вы хотите сразу
определить функцию, но дать ей возможность
быть переопределенной в наследниках
- присвойте модификатор virtual.
Последовательное
выполнение условий в switch блоке
возможно только если case условия
определены числами.
По поводу плюсика
в имени типа Enum - Enum, по логике, надо
объявлять в namespace, наравне с
классами. Однако, можно их запихать и внутрь
класса. Если их объявить в namespace - имя
типа будет построено как обычно, а если
внутри класса - то через плюсик.
Часть 2.
Чуть более сложные
вещи в языке C# и платформе .NET.
Глава 1. Модификаторы аргументов.
Регулярные выражения.
Рассмотрим
модификаторы аргументов
функций.
ref - передает
в качестве аргумента ссылку на объект.
Таким образом вы получаете возможность
изменять объект, не создавая новый, и не
возвращая его, как результат функции.
Пример:
private void Func1(ref string
str) {
str = "новая строка";
}
private void
MainFunc() {
string str = "строка";
Func1(ref str); // str = "новая
строка"
}
out - передает
в качестве аргумента ссылку на
неинициализированный объект. Таким
образом вы получаете возможность
возвращать множественные
результаты работы функции.
Пример:
private void Func1(out string
result1, out int result2, out object result3) {
result1 = 10;
result2 =
"результат";
result3 = new object();
}
private void
MainFunc() {
int r1;
string r2;
object r3;
Func1(out r1, out r2, out r3);
}
Разница между ref и out
только в том, что out не требует
инициализации аргумента до вызова
функции, а требует ее внутри функции. ref -
наоборот, требует передачи
инициализированного объекта.
param - очень хитрая
вещь, позволяет делать функции с
переменным количеством
аргументов. Требования просты - param
аргумент должен быть последним в списке
аргументов, аргументы должны быть
одного типа, аргументы задаются как
массив.
Например так:
private void Func1(byte b1,
param int[] data) {}
Если вам
необходимо, чтобы аргументы были
разнотипны - считайте их типом object. Еще
один момент - вместо списка аргументов в
строке, можно передавать массив... но тут
надо быть осторожным:
private void Func1(byte b1,
param object[] data) { }
private void
MainFunc() {
Func1(1, 1, "str", new
object()); // все нормально. data - массив типа
object из трех членов.
Func1(2, new object[] {1, "str",
new object()}); // все нормально, data - как в
предыдущем варианте.
Func1(3, 1, new object[]{"str",
new object()}); // data - массив типа object из двух
членов, второй - массив типа object из 2х
членов.
}
Регулярные выражения
В общем виде
регулярные выражения ( = regular
expressions = RegEx) - это шаблон
текстовой строки. Почти все задачи
касающиеся поиска и замены в строках
предпочтительно решать через них. Задачи
поиска и замены включают в себя поиск
текста, редактирование текста,
преобразование текста
(форматирование), выбирание
полезной информации из текста для
последующего использования и пр.
Пользоваться регулярными
выражениями имеет смысл когда объем
текста достаточно велик. Для работы с
регулярными выражениями
существует класс Regex
(System.Text.RegularExpressions.Regex).
Использоваться этот класс может
тремя путями:
1. пользоваться
статическими методами класса.
Предпочтительно, если программе это
редко нужно, и не при каждом запуске, а также,
если само выражение используется
только один раз подряд.
2. Создавать объект
класса regex в некомпилируемом виде
(флаг Compiled не установлен) и
пользоваться его функциями.
Предпочтительно если программе это редко
нужно, и не при каждом запуске, но выражение
используется несколько раз подряд.
3. Создавать объект
класса regex в компилируемом виде (флаг
Compiled установлен) и пользоваться его
функциями. Предпочтительно если
программа часто пользуется
выражением.
Регулярное выражение
состоит из шаблона и опций. С опциями
разобраться довольно просто, а вот
шаблоны - это отдельный язык. По поводу
задания шаблонов могу только
посоветовать пользоваться разными
вспомогательными программами типа
Expresso и пр.
Возьмем для примера
простой шаблон:
(?‹Protocol›\w+):\/\/(?‹Domain›[\w\.]+)\/?\S*
Этот шаблон
выдергивает из предложенного
текста все URL, и сохраняет их с
выделением протокола и
домена.
Код для
использования:
void RegexMatch() {
string inStr = "blah-blah-blah
something here http://www.domain.com/index.cgi and
some comments here and file here
ftp://ftp.domain123.info/repository/";
StringBuilder sb = new
StringBuilder();
Regex r = new
Regex(@"(?‹Protocol›\w+):\/\/(?‹Domain›[\w\.]+)\/?\S*",
RegexOptions.IgnoreCase
| RegexOptions.Singleline);
for (Match m = r.Match(inStr);
m.Success; m = m.NextMatch()) {
sb.AppendFormat("Найдено URL: {0},
протокол: {1}, домен: {2}\r\n", m.Value,
m.Groups["Protocol"].Value,
m.Groups["Domain"].Value);
}
textBox1.Text =
sb.ToString();
}
Результат будет:
Найдено URL:
http://www.domain.com/index.cgi, протокол: http, домен:
www.domain.com
Найдено URL:
ftp://ftp.domain123.info/repository/,
протокол: ftp, домен:
ftp.domain123.info
Примеров может быть
множество, лучше посмотрите шаблоны
в действии... а в Expresso есть система
интерактивного составления
шаблона... очень полезно для обучения, да и
примеров у них много.
Глава 2. Генеалогия
классов.
Итак, поговорим
когда кого и как нужно порождать.
Один из постулатов
современного высокоуровневого
программирования, не всегда верных,
впрочем, гласит, что "Чем меньше одинаковых
блоков кода в программе - тем лучше". Первое
следствие из этого постулата - блок
кода, который используется больше, чем в
одном месте программы должен быть вынесен в
отдельную функцию. Впрочем, это к теме не
относится. Второе следствие - набор
переменных/функций, которые
используются больше чем в одном классе
должны быть выделены в отдельный
родительский класс. В общем-то, этим правилом
можно руководствоваться при
определении "нужен ли вам некий родитель
для ваших классов?", если у вас их много.
Рассмотрим модельную
ситуацию с множеством классов, и
определим какие им нужны
родственные связи.
Довольно классический
пример - элементы управления.
Рассмотрим 4 элемента управления:
кнопка, текстовое поле, панель и группа. У
всех этих элементов должны быть свойства:
положение, размер, события нажатия,
фокуса и еще можно много всяких написать для
удобства программистов. Стало быть один
предок, общий для всех элементов управления
нашелся. Думаем дальше - панель и группа
должны иметь подчиненные элементы
управления, а также уметь прокручивать
собственную внутреннюю область. Стало
быть у этих двоих должен быть еще общий предок. Итак
структура получиться примерно
такой:
Ну вот вам общий принцип
выделения общих предков.
Рассмотрим вопросы
наследства, ограничения на
наследство, растранжиривания
наследства и стерилизации.
Многое из того, о чем
сейчас пойдет речь уже рассказывалось в части
1 главе 1 в разделе о декларациях.
Главное правило
наследования - каждый потомок
наследует все. Т.е. все члены класса,
открытые для наследования,
передаются каждому потомку в любом
колене. Единственное, что может
помешать - переопределение члена
класса.
По поводу "как жить
потомкам":
Итак, модификатор
abstract у класса говорит о том, что сам
класс ничего сделать не может, только
наследство осталось. Будем считать его
безвременно почившим
родственником... тем самым дядей "самых
честных правил", который однако весьма
четко описал как именно должны жить его
потомки. Тот же модификатор у члена
класса говорит о том, что класс считает этот член
обязательным у своих потомков, но сам толком
не знает что это такое. Эдакий старший
родственник, знающий как должны жить его
потомки, но сам живущий по-другому.
Модификатор virtual
у члена класса говорит о том, что класс знает что
делать, однако допускает иное
толкование для потомков. Хороший
родственник с широкими взглядами
:).
Теперь касательно доступа к
наследству:
модификатор private
обеспечивает неприкосновенность
наследства. Доступ есть только из других
унаследованных функций. Например, у
родителя есть открытая функция
public1, и закрытая функция
private1. Потомок может
обратиться только к public1, но если
внутри кода public1 есть обращение к
private1, то все будет работать.
модификатор
protected, в вопросах наследства,
эквивалентен public -
наследство открыто для доступа.
"Пользуйтесь, родственнички
дорогие!"
Теперь о собственном мнении
потомков по поводу наследства.
Вобщем-то, никто не
мешает потомкам выбирать из
наследства только понравившиеся
части. Да и посылать старших с их советами
"как надо жить" тоже. Любая функция,
объявленная virtual может быть
переопределена
модификатором override. Если
очень надо переопределить функцию не
отмеченную virtual - можно
использовать модификатор new.
Правда с этим надо быть осторожным - если уж вы
наследуете от кого-то, к вам будут
соответственно относится и
может быть конфликт с другими классами,
ждущими одной подписи функции, а
напарывающимися на другую. Да и от родни
не уйдешь - до оригинального варианта
функции все равно всегда можно
достучаться, если знать что он есть. Изнутри
потомка достаточно использовать
ключевое слово base, а снаружи -
преобразовать тип объекта в тип предка.
Предположим, функция Func1
переопределена в классе B,
потомке класса А с использование
слова new:
B b1 = new B();
b1.Func1(); // новый
вариант функции.
((A)b1).Func1(); // старый
вариант функции
И самое страшное -
стерилизация.
Очень просто
делается, как и все ужасы в нашем мире.
Модификатор класса sealed -
стерилизует его, и у него уже никогда не
будет детей :(. Тот же модификатор,
примененный к virtual функции
отменяет ее виртуальность.
И напоследок - крестные
(отцы/матери/дяди/тети/феи и пр. фольклорные
элементы)
В роли крестных
выступают интерфейсы. Их у каждого
класса может быть сколько угодно, и для того,
чтобы иметь крестных не обязательно иметь
родителей. Интерфейсы все очень строгие - они
точно знают что должен уметь делать их крес