Новинки языка C#.NET
ОГЛАВЛЕНИЕ
Введение
Когда говорят о .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)
Общие (или параметризованные) типы (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.