Продолжается подписка на наши издания! Вы не забыли подписаться?

C#

Нужен ли нам еще один язык?

Microsoft описывает C# как «простой, современный, объектно-ориентированный и типо-безопасный язык программирования, наследник С и С++». Такое утверждение не хуже подходит и к Java. Но, если сравнить эти два языка, предыдущее описание C# подходит и к Java. Как показывает следующий пример, возможности и синтаксис языков похожи.

Пример Hello World! на C#:

public class HelloWorldTest
{
   public static void Main (string[] args)
   {
      System.Console.WriteLine("Здравствуй, мир!");
   }
}

Аналогичный пример на Java:

public class HelloWorldTest
{
   public static void main (String[] args)
   {
      System.Out.println("Здравствуй, мир!");
   }
}

Но сходство идет дальше синтаксиса, ключевых слов и разделителей. Оно включает такие общие возможности Java и C#, как:

C#: эволюция Visual J++

С чего Microsoft решил, что нам нужен новый язык? Microsoft вложил прорву сил и средств в проект Visual J++, объявленный в октябре 1996 года. Эти усилия произвели на свет самую быструю JVM на рынке и Windows Foundation Classes (WFC), набор Java-классов, оберток для Win32 API. Не случайно Anders Hejisberg, руководитель проекта WFC (более известный, как автор Turbo Pascal), стал главным архитектором C#, ввиду чего C# немало унаследовал и от Turbo Pascal.

В Microsoft решили внести изменения в Java для более тесной интеграции с Windows. Ну, вы знаете, почему. Некоторые изменения – бесшовное сопряжение с COM, отказ от поддержки RMI и JNI и введение делегатов – привели к нарушению совместимости со стандартом Java. Вследствие этого Sun Microsystems предъявило иск Microsoft в октябре 1997 года за нарушение лицензионного соглашения. Это был приговор будущему микрософтовских разработок Java и Visual J++. Но в Microsoft решили использовать наработки в Java, компиляторах и JVM и преобразовать их в еще более амбициозный проект - Microsoft .NET.

Программы, написанные на C#, компилируются в промежуточный язык под названием "MSIL", с некоторой натяжкой его можно назвать эквивалентом байт-кода или р-кода Visual Basic. Как уже говорилось ранее, любой язык, который можно скомпилировать во MSIL, может воспользоваться такими возможностями CLR, как сборка мусора, отражения, метаданные, контроль версий, события и защита. Кроме этого, класс, написанный на одном языке, может наследовать от класса на другом языке и подменять его методы.

Интересно, что Microsoft продвигает кросс-языковую разработку, а Sun/Java – кросс-платформную.

К сожалению, и у того, и у другого обоих подхода есть свои проблемы. Создание компонентов на нескольких языках всегда влечет за собой проблемы взаимодействия. Более того, всегда есть проблема – что делать, когда ваш программист-постановщик задач найдет местечко потеплее. Кросс-платформная разработка тоже никогда не была безупречна, как знают Java-программисты, особенно в части GUI и потоков.

Сходство C# и Java

Не говоря о множестве других общих черт, большинство ключевых слов в Java имеют соответствия в С#. Некоторые ключевые слова совпадают, например, new, bool, this, break, static, class, throw, virtual и null. Этого можно было ожидать, поскольку данные ключевые слова заимствованы из С++. Занимательно, что многие ключевые слова Java, не имеющие прямых эквивалентов в C#, например, super, import, package, synchronized и final, просто называются по-другому в C# (base, using, namespace, lock и sealed, соответственно). В Таблице 1 приведены некоторые из ключевых слов и черт, существующих в обоих языках, но выглядящих слегка иначе в C#.

Таблица 1

 Косметические различия между Java и C#

  

Java

C#

Наследование

class Foo extends Bar
   implements IFooBar { }
class Foo: Bar, IFooBar { }

Ссылка на базовый класс

super.hashCode ();
base.GetHashCode ();

Использование внешних типов

import Java.util;
using System.Net;

Пространства имен

package MyStuff;
class MyClass { }
    
namespace MyStuff
{
   class MyClass {...}
}

Предотвращение наследования

final class Foo {}
sealed class Foo {}

Константы

final static int MAX = 50;
const int MAX = 50;

Комментарии

/**
* Convert string to
* uppercase.
*
* @param s The string to
* convert
*
* @return The uppercase
* string
*/
/// <summary>
/// Convert string to
/// uppercase.
/// </summary>
/// <param name="s">
/// The string to convert
/// </param>
/// <returns>
/// The uppercase string
/// </returns>

Устаревшие, не рекомендуемые к 
использованию классы или методы

@deprecated Не использовать!
[obsolete ("Не использовать!")]

Синхронизация

synchronized (this)
{ ++ refs; }
lock (this) { ++ refs; }

Класс Object

Другой хороший пример косметических различий двух языков – класс System.Object в C#, имеющий точно те же методы, что и Java-класс Java.long.Object, если не считать того, что они несколько иначе пишутся. Метод clone в Java называется MemberwiseClone в C#, equals из Java – это Equals в C#, getClass – getType, hashCode – GetHashCode, а toString - ToString.

Простое совпадение? Да, в недавнем интервью Adders Hejlsberg говорил, что "C# - это не клон Java". Совершенно верно, это MemberwiseClone.

Модификаторы доступа

В отличие от С++, в C# модификатор public и ему подобные относятся к одному определению (метода, класса, переменной) наподобие того, как это сделано в Java. Модификаторы public и private имеют одно и то же значение во всех трёх языках. Но "protected access" в Java называется "protected internal" в C#, а "package access" в Java называется "internal" в C#. Модификатор protected в C# дает наследникам доступ к членам класса, даже если наследники находятся в другой программе. Другое различие – в Java по умолчанию применяется package access, а в C# - private.

Исключения

Как и в Java, блоки try в C# поддерживают оператор finally. В C# нет оператора throws. Вас никто не заставляет обрабатывать хоть какие-то исключения. Как сказал один из сотрудников Microsoft, в том, что разработчики вынуждены обрабатывать исключения, больше вреда, чем пользы. Это приводит к большому количеству catch (Exception e)-обработчиков исключений, не делающих ничего полезного.

Есть мнение, что это неудачное решение, поскольку оператор throws делает очевидным, какие исключения возбуждаются методом и являются частью контракта метода. Иначе вы вынуждены читать исходный код вызываемого метода, чтобы узнать, какие исключения могут быть возбуждены. Но нам кажется, что при грамотной реализации такое решение может упростить программирование. Время покажет.

Что в C# лучше, чем в Java

Итак, C# и Java во многом выглядят и действуют похоже, зачем же вообще использовать C#? Как и следует ожидать от очередного языка, C# совершеннее Java в некоторых областях. В дополнение к появившимся новым возможностям, в C# проще синтаксис для таких вещей, как итерации, события и работа с примитивными типами вроде объектов и т.п., что уменьшает количество кода и ускоряет процесс разработки.

Отражения, метаданные и пользовательские атрибуты

Компиляторы и Java, и C# генерируют метаданные для поддержки отражения информации о типах (в угоду моде Microsoft переименовал typeinfo в reflection - отражение). Отражение дает возможность динамически получать информацию о типах. Это не уникальная для C# возможность. Подробнее о динамическом доступе к метаданным можно прочитать в разделе обзора, посвященном CLR.

Microsoft расширяет понятие метаданных пользовательскими атрибутами. О них тоже говорилось выше. Компилятор C# поддерживает атрибуты для взаимодействия с COM-объектами и Win32 API, сериализации объектов, условного исполнения кода. С помощью атрибутов можно также пометить метод как устаревший, не рекомендованный к использованию.

Пример того, где атрибуты полезны – определение XML-схемы для использования при сериализации класса:

using System.NewXml;
using System.Xml.Serialization;

[XmlRoot ("album", Namespace="music")]
public class Album 
{
   [XmlElement ("artist")]
   public string artist;

   [XmlElement ("title")]
   public string title;

   [XmlArray ("songs"), XmlArrayItem ("song")]
   public string [] songs;
}

Как вы можете видеть, класс непосредственно размечен атрибутами (задаваемыми в квадратных скобках), определяющими, какие конструкции XML используются при сериализации и десериализации объекта Album. Следующий листинг создаёт объект Album и сериализует его в XML.

using System.Xml.Serialization;
using System.IO;
class TestAlbum
{
   public static void Main ()
   {
      Album album = new Album ();
      album.artist = "Sasha";
      album.title = "Xpander";
      album.songs = new string[5];
      album.songs[0] = "Xpander Edit";
      album.songs[1] = "Xpander":
      album.songs[2] = "Belfunk";
      album.songs[3] = "Rabbitweed";
      album.songs[4] = "Baja";
      // Сохраняем состояние объекта в файл
      FileStream fs = new FileStream ("Album.xml", FileMode.Create);
      XmlSerializer serializer = new XmlSerializer(typeof(Album));
      serializer.Serialize (fs, album);
   }
}

Приведенный выше код генерирует следующий XML-файл:

<?xml version="1.0"? >
<album xmlns: xsi=http://vwv.w3.org/1999/XMLSchema-instance xmlns="music">
  <artist>Sasha</artist>
  <title>Xpander</title>
  <songs>
    <song>Xpander Edit</song>
    <song>Xpander</song>
    <song>Belfunk</song>
    <song>Rabbitveed</song>
    <song>Baja</song>
  </songs>
</album>

О том, как создавать собственные атрибуты, уже говорилось ранее.

Контроль версий

Контроль версий – очень нужная в Windows- и Java-разработке вещь. Java-разработчики не хуже остальных знакомы с устаревшими API и несовместимыми версиями сериализованных объектов.

.NET в очередной раз обещает устранить эти проблемы, позволяя указать зависимости версий между компонентами и поддержкой одновременного исполнения нескольких версий компонента. Более того, на компьютере может иметься несколько версий runtime .Net одновременно, что позволяет использовать в каждом случае ту версию, под которую компилировались компоненты. Это похоже на возможности контроля версий, оговоренные в Java Product Versioning Specification, за исключением того, что в .Net Framework принудительный контроль во время исполнения, компиляции и инсталляции уже встроен.

Средства отладки во время исполнения

Как уже говорилось ранее, среди стандартных библиотек CLR присутствует библиотека, дающая возможности runtime-отладки – вывода сообщения в консоль, выдача Assert-ов и т.п. Естественно, C# предоставляет полный доступ к этим возможностям, как, впрочем, и любой другой CLR-совместимый язык.

Assert-ы не поддерживаются в Java, но, как и над большинством других недоработок, в Java Community Process над этим работают :).

ref- и out-параметры

В C# параметры можно передавать по значению (in), по ссылке (ref), или указывать, что это – выходные параметры (out). Так как Java позволяет только передачу по значению, для передачи в метод чего-либо по ссылке приходится заниматься натуральным шаманством, например, использовать возвращаемые значения, передачу в одноэлементном массиве или помещение объекта в класс-обертку. Метод, принимающий параметр по ссылке в C#:

void swap (ref long n1, ref long n2);

Вызов C#-метода:

int i1 = 123, i2 = 321;
swap (i1, i2);

Аналогичный пример на Java:

void swap (long[] n1, long[] n2);

Вызов java-метода:

int i1 = 123, i2 = 321;
int[1] itmp1;
itmp1 = i1;
int[1] itmp2;
itmp2 = i2;
swap (itmp1, itmp2);

Необходимо снова подчеркнуть, что возможность определения параметров методов как прямых, передающихся по ссылке или обратно возвращаемых, заложена на уровне CLR, а конкретные языки только предоставляют некоторый синтаксис, позволяющий воспользоваться этими возможностями. Фирма Rational сейчас занимается портированием Java в CLR/.Net среду. Интересно, как они изобразят передачу ref- и out-параметров, снова через массивы, или будут передавать параметры по-человечески, но в судебном порядке?

Виртуальные методы

По умолчанию в Java все методы виртуальны и могут подменяться наследниками класса. C# более привержен традициям. В нем, как и в C++, виртуальные методы должны быть явно объявлены виртуальными. Более того, C# несколько улучшает модель декларации виртуальных методов, заставляя программиста явно указывать, что он хочет подменить реализацию базового класса. Это позволяет избежать трудноуловимых ошибок, часто встречающихся в C++- и Java-программах. Например, стоит в С++ при подмене методов неумышленно изменить сигнатуру метода базового класса (описать параметр как long вместо int), и вместо ожидаемого переопределения метода вы создадите дополнительную (перегруженную) реализацию метода. При этом базовый класс будет вызывать свою реализацию. Программист может часами смотреть на место, где допущена ошибка, и не видеть ее. C# интерпретирует неявную перегрузку как ошибку компиляции, требуя от производного класса использовать ключевое слово override для подмены виртуального метода. Кроме этого, при компиляции выдается предупреждение, если метод производного класса скрывает метод базового класса. В этом случае, чтобы убрать предупреждение, можно использовать ключевое слово new.

Перечисления (enums)

В отличие от C/C++, в Java не реализованы перечисления, так как создатели Java заявили, что перечисления не объектно-ориентированы. По правде говоря, enums куда безопаснее и удобнее, чем static final int в Java. Перечисления не только существуют в C#, они типо-безопасны, к ним можно применять операторы ++, -, <, и >, и они могут быть конвертированы в строку и обратно.

Тип данных decimal

В C# поддерживается типы данных decimal, пришедший в CLR из Automation и Visual Basic. decimal – 128-битный тип, имеющий большую точность и меньший разброс, чем типы с плавающей точкой. Он особенно полезен для финансовых приложений.

Выражения switch

Очень распространенной ошибкой С/C++-программистов, особенно начинающих, является пропущенный оператор break в конце раздела case оператора switch. Программист подсознательно считает, что в конце раздела case управление должно быть прервано и продолжиться после оператора switch. Но в С++ это не так, причем умышленно - чтобы позволить программисту создать один раздел с несколькими метками. В других языках обычно применяется перечисление меток через запятую в одном case-операторе. В 90 процентах случаев это удовлетворяет имеющимся потребностям, но реализация С++ более гибка. Разработчики C# нашли довольно красивое альтернативное решение.

По умолчанию при достижении конца case-раздела происходит выход из оператора switch. Чтобы достичь такой же функциональности, как в С++, можно применять оператор goto. В качестве меток используются соответствующие case-метки. Кроме того, с C# как метки можно использовать не только целые числа, но и строки (прямое заимствование из VB).

Делегаты и события

Делегаты – это объектно-ориентированные аналоги указателей на функции, предоставляющие типо-безопасный механизм для реализации функций обратного вызова и событий. Они могут указывать на статические методы или на нормальные методы класса. Надоело повторять, но и эта возможность является не особенностью языка, а унаследована от CLR, но именно в C# она наиболее органично встроена в язык (не потому ли, что для него она и делалась?).

Java работает с событиями через модель событий JavaBeans и классы-адаптеры. Обработка событий в C# проще и требует только реализовать индивидуальные методы вместо целых интерфейсов.

Как видно из этой статьи, прямым предком C# был Visual J++. Именно в нем появились так называемые делегаты и сама концепция делегирования. Именно эта замечательная возможность стала каплей, переполнившей чашу терпения Sun Microsystems. Как вы думаете, в каком смертном грехе Sun обвинил делегаты? Правильно, делегаты не объектно-ориентированны. Аргументы Sun можно прочесть на http://www.javasoft.com/docs/white/delegates.html, а ответ Microsoft – на http://msdn.microsoft.com/visualj/technical/articles/delegates/truth.asp (Некоторые предполагают, что вопрос о делегатах был поворотным пунктом, приведшим к Java-судилищу.).

Ключевое слово event вводит в ваш класс делегата, позволяющего рассылать события. Ниже приведен пример C#-программы, использующей события для оповещения Stock-Tracker'а об изменении курса.

using System;
// Вызывается при падении цены
delegate void PriceDecreasedDelegate(string name, long newPrice);
// Вызывается при росте цены
delegate void PriceIncreasedDelegate (string name, long newPrice);

class Stock
{
  // Содержит цену акций
  public Stock(string stockName, long startPrice) 
  { 
    name = stockName;
    price = startPrice;
  }
  public long Price 
  {
    get { return price; }
    set 
    {
      if (value > price && Fire_OnPriceIncreased != null)
        Fire_OnPriceIncreased(name, value); // Информируем клиентов
      else if (value < price && Fire_OnPriceDecreased ! = null)
        Fire_OnPriceDecreased (name, value); // Информируем клиентов
      price = value; // Update the price 
    }
  }

    // Данные 
    string name;
    long price;
    public event PriceDecreasedDelegate Fire_OnPriceDecreased = null;
    public event PriceIncreasedDelegate Fire_OnPriceIncreased = null;
}

class StockTracker
{
  // Выдает сообщение при изменении цены акций
  public StockTracker(Stock stock)
  {
    // Подключает наши обработчики к событиям класса Stock
    stock.Fire_OnPriceDecreased += new PriceDecreasedDelegate (OnPriceDecreased);
    stock.Fire_OnPriceIncreased += new PriceIncreasedDelegate (OnPriceIncreased);
  }
  // Сигнатура обработчика события должна соответствовать 
  // декларации делегата PriceDecreasedDelegate  
  private void OnPriceDecreased (string name, long val)
  {
    Console.WriteLine("Цена {0} упала до {1}!", name, val);
  }
  private void OnPriceIncreased(string name, long val)
  {
    Console.WriteLine("Цена {0} выросла до {!}!", name, val);
  }
}

class StockTester 
{
  // Проверка событий
  public static void Main() 
  {
    Stock ibm = new Stock("IBM", 100);
    StockTracker tracker = new StockTracker(ibm);
    // Обновить цену акций
    ibm.Price = 125; // Выдает "Цена IBM выросла до 125!"
    ibm.Price = 90; // Выдает "Цена IBM упала до 90!"
  }
}

Простые типы (Value-типы)

Как говорилось выше, все типы данных, поддерживаемые CLR, наследуются от базового класса object. Поэтому простые типы в C# (long, int, char) можно использовать везде, где необходима передача ссылки на объект. Java в данном случае проигрывает, поскольку простые типы в ней не рассматриваются как классы и в некоторых контекстах для работы с ростыми типами их приходится заворачивать в специальные классы-обертки, например, java.lang.Long. C#-компилятор неявно конвертирует простые типы в объекты (и наоборот) по потребности, через процесс под названием "упаковка и распаковка (boxing и un-boxing)". Причем если простые типы не рассматриваются как объекты, это не влечет никаких накладных расходов. Это позволяет C#-разработчикам смотреть на мир как на целостную систему типов. Ниже показан пример использования простых типов как объектов на Java:

int i = 10;
System.out.println((new Integer(i)).hashCode()); // Используется класс-обертка

А вот так это выглядит на C#:

int i = 10;
System.Console.WriteLine(i.GetHashCode()); // Скрытая конвертация 

Свойства

Хотя во многих языках свойства и поля выглядят одинаково, реально свойства и поля – это две большие разницы. Поле – это член данных класса, а свойство – это пара методов получения и предоставления доступа, предоставляющих доступ или вычисляющих нужные значения.

И C#, и Java (посредством JavaBeans) поддерживают работу со свойствами. Однако в C# свойства напрямую поддерживаются языком вместо использования отражений и соглашений именования, как в JavaBeans. Это дает краткий, легко читаемый синтаксис. Приведенный ниже пример показывает реализацию свойства Name с использованием поля name на Java:

String name;
public String getName() { return name; }
public void setName(String value) { name = value; }

Использование свойства:

obj.setName ("Marc");
if (obj.getName() != "Marc")
   throw new Exception("Это не я!");

и на C#:

string name;
public string Name 
{
  get { return name; }
  set { name = value; } // 'value' – это новое значение
}

Использование свойства:

obj.Name = "Marc";
if (obj.Name != "Marc")
  throw new System.Exception ("Это не я!");

Преимущества стиля C# в том, что методы get и set находятся в том же блоке кода и в том, что для обращения к свойствам используется более интуитивный синтаксис, поскольку доступ к свойствам производится точно так же, как к нормальным полям. Свойства JavaBeans тоже выглядят как нормальные поля, когда к ним обращаются из скриптов или визуальных дизайнеров, но не при доступе из Java-кода.

Индексируемые свойства и свойства по умолчанию

Кроме переопределения операторов, в C# можно определять индексируемые свойства. К тому же, индексируемые свойства можно помечать как свойства по умолчанию для класса. Это позволяет рассматривать объект как массив, каждый элемент которого становится доступным с помощью операторов доступа к свойствам (get и set). Ниже приведен пример класса с индексированным свойством по умолчанию.

public class Skyscraper
{
  Story[] stories;
  public Story this [int index]
  {
     get { return stories [index]; }
     set
     {
       if (value != null)
         stories [index] = value;
     }
  }
  ...
}

Применение класса с индексированным свойством по умолчанию:

Skyscraper empireState = new Skyscraper (...);
empireState[102] = new Story ("The Top One", ...);

Массивы, коллекции и итерации

Выражение foreach в C# (украденное из VB) позволяет легко перечислять классы, поддерживающие интерфейс Enumerable, включающий массивы и коллекции. Это снимает потребность писать for(int i=0; i < ary.length;i++) и триаду getIterator/hasNext/next как в Java и C++. Следующий листинг демонстрирует итерации на Java:

import java.util.*;
class Iterate 
{
  public static void main (String [] args) 
  {
    // Перебор массива аргументов командной строки
    for (int i = 0; i < args.length; i++) 
      System.out.println(args [i]);

    // Создание связанного списка
    LinkedList list = new LinkedList();
    list.add ("Элемент 1");
    list.add ("Элемент 2");

    // Перебор значений списка 
    Listlterator it = list.listlterator (0);
    while (it.hasNext())
      System.out.println (it.next());
  }
}

И на C#:

using System.Collections;
class Iterate 
{
  public static void Main(string [] args) 
  {

    // Перебор массива аргументов командной строки
    foreach (string arg in args)
      System.Console.WriteLine (arg);
    // Создание связанного списка
    ObjectList list = new ObjectList ();
    list.Add("Элемент 1");
    list.Add("Элемент 2");
    // Перебор значений списка 
    foreach (string str in list)
      System.Console.WriteLine (str);
  } 
}

Форматирование строк

Хорошим примером синтаксических улучшений в C# служит форматирование строк. Java предоставляет класс MessageFormat для форматирования в стиле printf. Синтаксис C# куда чище потому, что функции можно передать переменное число параметров. Приведенные ниже примеры 4 показывает возможности форматирования строки и переменные параметры C# и Java. Оба примера выдают "Error: File not found. (Code 2)".

Java-вариант:

// Создаем массив типа Object, содержащий значения для вывода
Object args [] = { "File not found.", new Integer (2) };
// Формирование и вывод сообщения
System.out.println (Java.text.MessageFormat.format (
                              "Error: {0} (Code {1})", args));

C#-вариант:

System.Console.WriteLine ("Error: {0} (Code {1})", "File not found.", 2);

Перегрузка операторов и операторы приведения типов

Перегрузка операторов позволяет переопределить их семантику (+, -,+ =, == и т.д.) для некоторого типа. При правильном использовании перегрузка операторов может обеспечить интуитивный синтаксис для работы с пользовательскими типами как с типами-примитивами. В C# для этого предусмотрено ключевое слово operator.

Кроме перегрузки операторов, C# позволяет определить операторы приведения типов. Например, класс с названием Fraction может предоставлять оператор приведения типа, конвертирующий Fraction в double.

В C# оператор == - это невиртуальный (операторы не могут быть виртуальными) метод класса object, который сравнивает два объекта. Если класс предполагается использовать в некоторых списковых структурах, поддерживающих сортировку (например, коллекциях), следует реализовать интерфейс IComparable. Этот интерфейс должен реализовать единственный метод CompareTo (object), возвращающий отрицательное, положительное или нулевое значение. Если же вы хотите, чтобы с классом было удобнее работать, можно реализовать операторы сравнения <, <=, >=, >. Стандартные типы обычно реализуют интерфейс IComparable, и для них по умолчанию доступны операторы сравнения.

Вот простой пример того, как работать с IComparable и операторами сравнения:

// Класс, реализующий IComparable и операторы сравнения
public class Score : IComparable
{
  int value;

  public Score (int score)
  {
    value = score;
  }

  public static bool operator == (Score x, Score y)
  {
    return x.value == y.value;
  }

  public static bool operator != (Score x, Score y)
  {
      return x.value != y.value;
  }

  public int CompareTo (object o)
  {
    return value - ((Score)o).value;
  }
}

Пример использования вышеописанного класса

Score a = new Score (5);
Score b = new Score (5);
Object c = a;
Object d = b;

Сравнение a и b по ссылке:

System.Console.WriteLine ((object)a == (object)b; // false

Сравнение a и b по значению:

System.Console.WriteLine (a == b); // true

Сравнение ссылок c и d:

System.Console.WriteLine (c == d); // false

Сравнение ссылок c и d по значению объектов:

System.Console.WriteLine (((IComparable)c).CompareTo (d) == 0); // true

Интерфейсы

Интерфейсы C# похожи на интерфейсы Java, но C# дает большую гибкость при их использовании. Класс может реализовать методы интерфейса явно или неявно:

public interface ITeller
{
  void Next ();
}

public interface IIterator
{
  void Next ();
}

public class Clark : ITeller, IIterator
{
  void ITeller.Next () { ... }
  void IIterator.Next () { ... }
}

Это дает классу двойную выгоду. Во-первых, класс может реализовать несколько интерфейсов, не беспокоясь о конфликтах имен. Во-вторых, класс может «спрятать» метод, если он не должен быть доступен всем пользователям класса. Чтобы получить доступ к методам, при реализации которых было явно указано имя интерфейса, необходимо использовать явное приведение типов:

Clark clark = new Clark ();
((ITeller)clark).Next();

Работа с указателями

В C# имеется ключевое слово unsafe. В методах, помеченных этим ключевым словом, возможна прямая работа с памятью: адресная арифметика, ручной контроль над выделяемой памятью и т.п. Если указатель ссылается на объект, управляемый сборщиком мусора, компилятор задействует слово fixed, чтобы «закрепить» объект. Сборщик мусора перемещает объекты в памяти, но если это делать при работе с указателями, указатели с большой вероятностью начнут указывать на мусор. Выбор слова "unsafe" весьма удачен, так как предостерегает программиста от использования указателей там, где без них можно обойтись.

Многомерные массивы

C# позволяет создавать как вложенные, так и многомерные массивы. Вложенные массивы очень похожи на реализацию массивов в С++ и Java. Многомерные массивы обеспечивают более точное и эффективное решение некоторых задач. Пример такого массива:

int [,,] array = new int [3, 4, 5]; // creates 1 array
int [1,1,1] = 5;

Использование вложенных массивов:

int [][][] array = new int [3][4][5]; // creates 1+3+12=16 arrays
int [1][1][1] = 5;

Производительность C#

Несколько свойств C# приводят к улучшенной по сравнению с Java производительности. Это, во-первых, отсутствие интерпретации. Пришествие JIT-компиляторов означает, что байт-код Java обычно компилируется JVM в "родной" код вместо интерпретации. Различие с Common Language Runtime в том, что здесь байт-код никогда не интерпретируется, а всегда JIT-компилируется. Уже на стадии отладки можно воспользоваться функциями дизассемблирования, встроенными в VS.Net, для отладки на уровне машинных кодов (см. рисунок 10). Интересно, что машинные коды генерируются уже оптимизированными. Так, наш простенький примерчик, выполняющий два вложенных цикла и несколько математических операций, при дизассемблировании вообще не обращался к стеку, работая только с регистрами. Сравнение с тем же кодом, скомпилированным на VC 6, показало, что код, сгенерированный компилятором C# в debug-режиме, значительно обгонял аналогичный С++-код, также скомпилированный в debug-режиме. Когда мы скомпилировали этот же С++-код в release-режиме, его скорость была примерно той же, что и скорость C#. Объясняется это тем, что компилятор С++ не производит оптимизации в debug-режиме. C#-код мы не могли скомпилировать в release-режиме, поскольку бета 1 этого не позволяет (при попытке запуска release-версии возникали исключения), но, по всей видимости, скорость вряд ли значительно возрастет. Скорее всего, высокая производительность C#-кода объясняется тем, что реальную компиляцию производит не компилятор C#, а компилятор среды CLR, о котором столь расплывчато говорит Microsoft. Реальная производительность "родных" С++-приложений будет все-таки выше, так как С++ работает с памятью менее витиеватым способом и обычно не использует разные runtime-проверки типа проверок выхода за границы массива и сборки мусора.

Java же, по сути, так и осталась интерпретируемым языком. Нормальной ее прекомпиляции мешают внутренние проблемы типа не рассчитанного на прекомпиляцию формата байт-кода, тесной интеграции RTTI с байт-кодом и т.п. Самый быстродействующий JIT-компилятор был создан Microsoft для своей Java-машины, но даже он не мог на равных конкурировать по скорости с кодом, создаваемым нормальными компиляторами. Благодаря не в последнюю очередь Sun, теперь не приходится ожидать от Microsoft каких-либо шагов по развитию JIT-компиляции для Java.

Рис. 10. Фрагмент дизассемблированного C#-кода

Также необходимо учитывать, что изначальная поддержка Win32 и COM производительнее J/Direct и Java Native Interface.

C# поддерживает операцию stackalloc. Операция stackalloc похожа на выделение памяти с помощью new, за тем исключением, что она размещает память для массива в стеке, а не в хипе. Стековое размещение гораздо шустрее хипового, и не нуждается в сборке мусора.

Тип данных struct – это вылитый объект, не считая того, что struct'ы – это типы-значения, а не ссылочные типы и не поддерживают наследования. Они могут быть размещены в стеке как простые типы и не дают лишней нагрузки на vtable. Используйте их для представления простых типов вроде Point, Rect, Fraction и т. д.

Код C# может быть помечен как unsafe, чтобы позволить прямой доступ к памяти и предотвратить вмешательство сборщика мусора.

Что плохо?

Ни Java, ни C# не поддерживают шаблонов. Но в Java Community Process уже внесено такое предложение, и, говорят, Microsoft Research в Кембридже работает над аналогичным решением для .NET.

Как и J/Direct, библиотека классов P/Invoke дает C# способность общаться напрямую с Win32 API и DLL. Для взаимодействия это хорошо, но требует писать прототипы функций C# с теми же сигнатурами, что и функции API. Например, обработчики (хотя бы HWND) апроксимируются с использованием int из C#, а указатели – через параметры ref. Такая техника подвержена ошибкам сигнатур, приводящим к сбоям и непредсказуемому поведению. При исследовании VS.Net мы наткнулись на недокументированные библиотеки, содержащие описания Windows-типов, таких, как handler-ы, структуры и т.п. Эти библиотеки содержали также описания некоторых Win32-функций. Нам не удалось подключить эти библиотеки к нашим проектам, однако само их наличие говорит, что такие библиотеки станут рано или поздно доступны вне зависимости от желания Microsoft.

Аналогично P/Invoke, СОМ Interop предоставляет классы-обертки, позволяющие C# использовать COM-объекты и наоборот. Увы, некоторые типы COM-интерфейсов не на 100% совместимы с обертками.

Здесь приведен пример создания компонента с помощью VS.Net и языка C#. Еще один пример использования C# можно найти в статье «Регулярные выражения»


Copyright © 1994-2016 ООО "К-Пресс"