Когда говорят о .NET языках, прежде всего подразумевают C# - язык
появившийся вместе с первой версией .NET Framework и являющийся основным
языком .NET. Почему это там можно вести длительные споры и приводить
множество аргументов, но основным из них будет то, что язык специально
создавался под новую платформу, поэтому с момента своего рождения он
поддерживал все концепции платформы .NET.
В этой статье я расскажу о новшествах в языке C#, которые были
привнесены в язык с выходом первой Beta версии .NET Framework 2.
Основная задача статьи - рассказать разработчикам о новых замечательных
возможностей, доступных при разработке программ на C#.
Внешние псевдонимы (external aliases) позволяют использовать
различные сборки с одинаковыми пространствами имен как различные
пространства имен. Звучит запутанно, но на самом деле идея проста,
каждой отдельной сборке назначается свое глобальное пространство имен.
Например у нас есть две сборки Functions.dll и Globals.dll, каждая из
которых содержит пространство имен PublicFunctions.
Сборка Functions.dll
namespace PublicFunctions
{
public class Functionality
{
//функции сборки Functions.dll
}
}
Сборка Globals.dll
namespace PublicFunctions
{
public class Functionality
{
//функции сборки Globals.dll
}
}
Как видите, при использовании двух этих сборок может возникнуть
конфликт, поэтому мы назначаем каждой из сборок "внешнее пространство
имен" по которому и будем обращаться к функциям той или иной сборки.
extern alias Functions;
extern alias Globals;
public class Main
{
public Main()
{
Functions.PublicFunctions.Functionality.SomeMethod();
Globals.PublicFunctions.Functionality.AnotherMethod();
}
}
Как некоторое дополнение и расширение предыдущего параграфа статьи
нужно сказать о новой возможности по работе с псевдонимами для
пространств имен. Например, чтобы обратиться к глобальному пространству
имен можно использовать синтаксис global::namespace.
global::Microsoft.Win32;
А для того, чтобы обратиться к псевдониму пространства имен, а не
одноименному классу нужно использовать синтаксис
MyAlias::MySubNamespace.SomeClass.
using Win32 = Microsoft.Win32;
// ...
Win32::SystemEvents.CreateTimer(100);
Общие (или параметризованные) типы (generics) позволяют
при описании классов, структур, методов и интерфейсов использовать
параметризованные параметры (не указывать тип параметра в момент
написания кода). Тип параметра определяется в момент объявления
переменной соответствующего типа. Таким образом можно создать некоторый
общий элемент, тип который можно использовать в дальнейшем для данных
различных типов. Программисты на C++ могут углядеть с общих типах
сходство с шаблонами (templates), в чем-то эта аналогия будет
верна, но тут существуют некоторые ограничения, которые мы рассмотрим
чуть позднее.
Объяснение получилось несколько сумбурным, поэтому будет лучше
рассмотреть простые и понятные примеры. Прежде всего создадим класс
используя общие типы:
class Generics<TYPE1, TYPE2>
{
private TYPE1 mVar1;
private TYPE2 mVar2;
public Generics(TYPE1 Var1, TYPE2 Var2)
{
this.mVar1 = Var1;
this.mVar2 = Var2;
}
public string ToStringFunction(string Delemiter)
{
return this.mVar1.ToString() + Delemiter + this.mVar2.ToString();
}
public TYPE1 Variable1
{
get
{
return this.mVar1;
}
set
{
this.mVar1 = value;
}
}
public TYPE2 Variable2
{
get
{
return this.mVar2;
}
set
{
this.mVar2 = value;
}
}
}
Как видно из примера, для того чтобы использовать общие типы нужно
после объявления класса указать параметризованные типы: Generics<TYPE1,
TYPE2> объявляет класс с двумя параметризованными типами. Теперь
используем написанный класс:
// объявление
Generics<string, string> strGeneric = new Generics<string, string>("Hello","world");
Generics<int, int> intGeneric = new Generics<int,int>(1, 2);
Generics<string, int> strintGeneric = new Generics<string,int>("Three", 3);
int intSum;
string strSum;
// использование
intSum = intGeneric.Variable1 + intGeneric.Variable2;
strSum = strintGeneric.Variable1 + " " + strintGeneric.Variable2.ToString();
MessageBox.Show("\nstrGeneric:\n" + strGeneric.Variable1 + " " + strGeneric.Variable2 +
"\n\nintGeneric sum:\n" + intSum.ToString() +
"\n\nstrintGeneric sum:\n" + strSum.ToString());
Таким образом очевидно, что создан класс который можно использовать с
параметрами различных типов, которые устанавливаются в момент объявления
класса. Аналогичным образом можно объявить структуру или интерфейс:
public struct GenericStruct<TYPE>
{
public TYPE someField;
}
public interface IGeneric<TYPE>
{
TYPE SomeMethod();
TYPE AnotherMethod();
}
Более того, параметризованные типы могут быть использованы при
объявлении делегатов функций. Продемонстрирую эту возможность используя
объявленный выше класс Generics.
// Объявляем делегат
public delegate DELEGATETYPE GenericDelegate<DELEGATETYPE, PARAMTYPE> (PARAMTYPE Param);
// используем делегат
Generics<string, string> strGeneric = new Generics<string, string>("Hello", "world");
GenericDelegate<string, string> genDelegate =
new GenericDelegate<string, string>(strGeneric.ToStringFunction);
// вызов делагата
MessageBox.Show(genDelegate(" my "));
"Вау!" - воскликнут программисты на C++ использующие в своей работе
также и C#. "И что нам с того?" - скажут программисты на C# никогда не
работавшие с шаблонами С++. Какие же преимущества дает использование
общих типов?
- Наиболее очевидное - повторное использование кода. Нет
необходимости создавать два идентичных класса, отличающихся только
типами параметров, достаточно создать один с параметризованными
типами. При этом использование параметризованных типов позволяет
создавать единый программный код для работы с различными типами
данных. Например, единожды написанный алгоритм может работать и с
целыми числами и с числами с плавающей десятичной точкой, при этом
не производя на каждом шаге проверку/приведение типа. Так Generics
вытесняют классы объявленные с использованием типа object.
- Повышение производительности кода по сравнению с использование
параметров типа object - нет необходимости выполнять приведение, как
уже сказано выше, на каждом шаге, за счет чего получается выигрыш в
производительности.
- Проверка типов в момент компиляции программы. Поскольку не
используются параметры типа object, то компилятор может выполнить
проверку типа каждого параметра в момент компиляции, поскольку типы
для Generic классов жестко задаются в момент объявления переменных
классов этого типа.
К сожалению, опытных программистов на C++ я должен несколько
расстроить. Общие типы все-таки не соответствуют шаблонам в C++,
поскольку параметризованные типы в C# не могут иметь типов по умолчанию.
Параметризованные типы не могут быть использованы в качестве базовых
классов для общих типов. Также в C# не допускается использования Generic
классов в качестве параметров типов в других Generic классах.
Но, несмотря на это, общие типы все-таки весьма полезным
новшеством языка C#, особенно ценным и удобным для разработчиков
использующих математические алгоритмы, поскольку преимущества
использования Generic классов очевидны.
Статические классы (static) - классы содержащие только статические
функции. Например, некоторый класс позволяющий получить доступ к
настройкам приложения хранимым в реестре или в базе данных может быть
объявлен как:
public static class AppSettings
{
public static string BaseDir
{
}
public static string GetRelativeDir
{
}
// и т.д. и т.п.
}
Экземпляр такого класса не может быть создан с использованием
оператора new, поскольку все члены этого класса статические и доступны с
использованием имени этого класса, например AppSettings.BaseDir и т.п.
Разделение классов (partial types) позволяет разбивать код класса на
несколько различных частей. Например, мы имеем класс User:
public class User
{
private int mInt;
private long mLong;
private string mString;
private bool mBool;
public int MyInt{
get{return this.mInt;}
set{this.mInt = value;}
}
}
Мы можем разбить его на несколько частей используя ключевое слово
partial:
public partial class User
{
// здесь помещается генерируемый код
private int mInt;
private long mLong;
private string mString;
private bool mBool;
}
public partial class User
{
// здесь помещается код, написанный вручную
public int MyInt{
get{return this.mInt;}
set{this.mInt = value;}
}
}
Для компилятора эти два способа описания класса равнозначны. Тогда
зачем нужна такая возможность разделять класс? Прежде всего для
отделения кода создаваемого генератором кода и разработчиком, как
например это сделано для поддержки дизайнера форма в Visual Studio 2005.
Если вы обратите внимание, то заметите, что код располагающий элементы
на форме по умолчанию скрыт и отделен от код написанного вами как раз с
использованием ключевого слова partial.
Также подобная возможность может быть использована для разделения
функций класса по нескольким файлам, что позволит одновременно работать
с разными частями класса нескольким разработчикам.
Общеизвестно, что для того чтобы перебрать все элементы в некоторой
коллекции используется метод foreach. Для тех, кто никогда не создавал
собственные коллекции позволяющие перебирать элементы этот механизм был
неизвестен, поэтому стоит описать процесс создания коллекции,
поддерживающей последовательный перебор элементов с помощью синтаксиса
foreach.
Для того, чтобы коллекция поддерживала foreach необходимо реализовать
метод GetEnumerator, возвращающий специальный класс с помощью которого
производится определения порядка вывода элементов. Для примера создадим
коллекцию, при проходе с помощью foreach которой, элементы возвращаются
в порядке их расположения в массиве.
public class MyUsualCollection
{
public int[] myItems;
public MyUsualCollection()
{
myItems = new int[10] { 1,2,3,4,5,6,7,8,9,10 };
}
public MyUsualEnumerator GetEnumerator()
{
return new MyUsualEnumerator(this);
}
}
// Класс Enumerator для нашей коллекции
public class MyUsualEnumerator
{
int indexEnum;
MyUsualCollection myCol;
public MyUsualEnumerator(MyUsualCollection col)
{
this.myCol = col;
indexEnum = -1;
}
public bool MoveNext()
{
indexEnum++; // перемещаемся дальше
return (indexEnum < this.myCol.myItems.GetLength(0));
}
public int Current
{
get
{
return (this.myCol.myItems[indexEnum]);
}
}
}
Очевидно, что для такой простой операции слишком много кода.
Поэтому-то в C# с выходом Visual Studio 2005 и Framework 2 появился
более простой путь поддержки перебора элементов. Того же результата мы
добьемся написав следующий код:
public class MyIteratorCollection
{
public int[] myItems;
public MyIteratorCollection()
{
myItems = new int[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
}
public IEnumerator GetEnumerator()
{
for (int i = 0; i < 10; i++)
yield return myItems[i];
}
}
Согласитесь, когда компилятор берет вашу работу на себя это приятно!
Безымянные методы (anonymous methods) позволяют значительно сократить
объем кода, который должен написать разработчик. Наиболее простое и
понятное использование безымянных методов при назначении относительно
простых обработчиков событий. Рассмотрим пример, пусть у нас есть форма,
на которой размещены текстовое поле txtLogin и кнопка btnLoginи нам
нужно, чтобы при изменении текста в текстовом поле, изменялся текст
кнопки. Разумеется, что для этого необходимо в обработчике события
TextChanged изменять текст кнопки.
Какой код создает Visual Studio при добавлении нового обработчика
события? Прежде всего функцию обработчик и запись о соответствии функции
обработчика событию, что осуществляется присвоением соответствующего
делегата соответствующему событию контрола TextBoxв функции
InitializeComponent:
this.txtLogin.TextChanged += new System.EventHandler(this.txtLogin_TextChanged);
Сам обработчик выглядит так:
private void txtLogin_TextChanged(object sender, EventArgs e)
{
btnLogin.Text = "Login [" + txtLogin.Text + "]";
}
Те же операции можно выполнить вручную создав, например, такой код:
public frmMain()
{
InitializeComponent();
this.txtLogin.TextChanged += new System.EventHandler(this.txtLogin_TextChanged);
}
private void txtLogin_TextChanged(object sender, EventArgs e)
{
btnLogin.Text = "Login [" + txtLogin.Text + "]";
}
Теперь же перепишем этот код с использованием безымянных методов:
public frmMain()
{
InitializeComponent();
this.txtLogin.TextChanged += delegate { btnLogin.Text = "Login [" + txtLogin.Text + "]"; };
}
Таким образом мы поместили код обработчика непосредственно в место
присваивания делегата событию TextChanged. Формально этот процесс ничего
не меняет - в момент компиляции компилятор сам создаст функцию и
присвоит ей некоторое имя, а потом создаст делегат для этой функции и
присвоит его событию TextChanged. Но отметьте, на сколько меньше строк
кода нужно для реализации, при этом никаких потерь производительности
после компиляции не будет, поскольку в результате будет получен такой же
код, что и в примерах описанных выше.
Конечно, приведенный пример практической ценности не имеет (а если и
имеет, но весьма и весьма небольшую), но демонстрирует общий путь
использования безымянных методов. При этом ничто не мешает получить
доступ к передаваемым функции параметрам, для этого нужно лишь явно
указать их при написании безымянного метода:
this.btnLogin.Click += delegate(object sender, EventArgs e)
{MessageBox.Show(((Button)sender).Text.ToString());};
В дополнение приведу более общий пример использования безымянных
методов:
public delegate int MyDelegate(int mInt1, int mInt2);
private void btnAnonymous_Click(object sender, EventArgs e)
{
MyDelegate anSum = delegate(int a, int b) { return a + b; };
MessageBox.Show(anSum(1, 2).ToString());
}
В примере новый делегат создается непосредственно в обработчике
события щелчка по кнопке.
В C# с выходом .NET Framework 2 и Visual Studio 2005возможно
ограничивать видимость get и set блоков свойств классов, например:
public partial class User
{
public int MyInt{
get{return this.mInt;}
private set{this.mInt = value;}
}
}
Таким образом только внутри класса можно будет установить свойство
MyInt, "извне" такая возможность будет недоступна. Такой подход упрощает
код доступа к свойствам, больше нет необходимости обращаться к
внутренней переменной, достаточно сделать блок set недоступным для
"внешнего воздействия" и обращаться к этой переменной используя
свойство.
В этой статье я рассказал о некоторых новшествах в языке C#. Конечно,
эта статья не претендует на полноту изложения и описания перечисленных
новинок. Цель статьи - дать информацию о новых возможностях доступных
разработчику. За подробным описанием я советую обратиться к
соответствующим монографиям посвященным C# и MSDN.
Я с радостью приму предложения и комментарии читателей! Пишите
gaidar at vbstreets.ru и
я постараюсь ответить каждому.