• Основы
  • Идентификаторы
  • Стандарты именования
  • Ключевые слова
  • Входные и выходные данные
  • Компиляция
  • Пространства имен
  • Создание и добавление библиотек при компиляции
  • Обнаружение и разрешение
  • Строгие имена и глобальный кэш
  • Типы данных
  • Простые типы
  • Типы перечислений
  • Структуры
  • Ссылочные типы
  • Операторы
  • Присваивание
  • Сравнение
  • Операторы равенства aрифметические, условные, побитовые, битового дополнения и сдвига
  • Преобразование типов
  • Перезагрузка
  • sizeof и typeof
  • Делегаты
  • Подробно о классах
  • Модификаторы
  • Конструкторы
  • Методы
  • Свойства и индексаторы
  • События
  • Исключения
  • Условная компиляция
  • Вопросы безопасности
  • Заключение
  • Пpиложение B

    C# для разработчиков Java

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

    При первом взгляде на код C# может показаться, что он не слишком впечатляющий, так как обнаруживается отчетливое сходство между ним и Java. Однако, если вы ожидали каких-то существенных изменений, то правда состоит в том, что на самом деле существует не так уж много синтаксических различий. Пути двух языков расходятся во внутренних тонкостях таких вещей, как перегрузка операторов, индексаторы, делегаты, свойства и перечисления с контролем типов. При более внимательном рассмотрении можно понять, что в конце концов между ними существует большое различие. (Приведенные выше темы будут подробнее рассмотрены позже в этом приложении.)

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

    Основы

    Одно из основных различий между C# и Java лежит не в самом языке, а в платформе, поверх которой они реализованы. Программам Java требуется для выполнения кода рабочая среда времени выполнения Java Runtime Environment. C# и, на самом деле, вся платформа .NET выполняются в среде Common Language Runtime.

    Большинство свойств CLR, внутреннее управление памятью, согласованность среды, масштабируемость и независимость от базовой платформы отражены в JRE Java. В то время, как JRE ограничена исключительно одним языком Java, CLR предоставляет поддержку и интеграцию нескольких языков с помощью VOS (virtual object system — система виртуальных объектов), которая предоставляет богатую типами систему, предназначенную для реализации множества различных типов языков программирования. Исходный код Java можно компилировать в промежуточное состояние, называемое байт-кодом. Он может затем выполняться с помощью поставляемой виртуальной машины. CLR, наоборот, не предоставляет виртуальную машину. Код C# также компилируется в промежуточное состояние, называемое для удобства промежуточным языком (IL, Intermediate Language). Но код IL передается в управляемые CLR процессы выполнения или компиляторам JIT CLR, обычно называемым JITters, которые преобразуют по требованию разделы IL в собственный код.

    Давайте рассмотрим известный пример "Hello, World!" на Java (который будет показан здесь без затенения): 

    public class Hello {

     public static void main(String args[]) {

      System.out.println("Hello world! This is Java Code!");

     }

    }

    Соответствующий код C# для этого примера следующий (представлен на сером фоне):

    public class Hello {

     public static void Main(string[] args) {

      System.Console.WriteLine("Hello world! This is C# code!");

     }

    }

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

    string
    и
    Main
    и использование
    System.Console.WriteLine
    вместо
    System.out.println
    ). Он является, с другой стороны, по-прежнему зависимым от регистра символов.

    Необходимо отметить тип

    string
    , который в C# также может записываться с заглавной S — как
    String
    . Следовательно, приведенный выше код можно записать в следующем виде:

    public class Hello {

     public static void Main(String [] args) {

      System.Console.WriteLine("Hello world! This is C# code!");

     }

    }

    Можно заметить, что спецификатор ранга массива

    []
    перемещен из позиции перед переменной
    args
    в примере Java в положение между типом
    string
    и переменной
    args
    в примере C#. В C# спецификатор ранга массива должен появляться перед именем переменной, так как массив является на самом деле типом данных, что указывается с помощью
    []
    :

    // C#

    int [] X; // массив целых чисел в C#


    // пример java

    int [] х; // массив целых чисел в java

    int x[]; // массив целых чисел в java

    Тот же самый код C# можно также представить следующим образом:

    using System;

    class Hello {

     public static int Main() {

      Console.WriteLine ("Hello world! This is C# code!");

      return 0;

     }

    }

    Как можно видеть, изменилось несколько элементов. Объявление

    string [] args
    в сигнатуре метода является в C# необязательным (хотя скобки должны стоять перед именем параметра), как и использование public при объявлении метода. Ключевое слово
    using
    аналогично по смыслу ключевому слову
    include
    в Java, и так как
    System
    является используемым по умолчанию включением в C#, первая строка позволяет нам опустить
    System
    , которое находилось перед
    Console.WriteLine
    . Поскольку мы используем в этом примере
    int
    вместо
    void
    , необходимо включить строку
    return 0;
    .

    Блоки кода C# заключаются в фигурные кавычки, также как в Java. Можно сказать, что метод

    Main()
    является частью класса, так как он заключен в фигурные кавычки. Точкой входа в приложении C# является метод
    static Main
    , как требует компилятор. Необходимо отметить, что Java использует
    main()
    в нижнем регистре. Подчеркнем также, что только один класс в приложении может иметь
    Main
    . Модификатор доступа
    public
    (обсуждаемый позднее) объявляет метод доступным потребителям кода вне класса, пакета или приложения, и также, как и компилятор, требует сделать метод видимым.

    Аналогично в Java ключевое слове

    static
    позволяет вызывать метод сначала без создания экземпляра класса. Для метода
    Main()
    можно выбрать в качестве возвращаемого типа значения
    void
    или
    int
    .
    void
    определяет, что метод не возвращает значение, a
    int
    определяет, что он возвращает целое значение.

    Идентификаторы

    Ключевые слова, рассматриваемые в следующем разделе, не могут служить идентификаторами ни в Java, ни в C#, однако в C# можно использовать ключевые слова как идентификаторы, помещая перед ними символ

    @
    . Отметим, что это исключение имеет отношение только к ключевым словам и не нарушает другие правила. Оба языка являются зависимы ми от регистра символов, поэтому идентификаторы должны иметь согласованное использование заглавных букв. Хотя идентификаторы могут содержать буквы и цифры, первый символ идентификатора как в C#, так и в Java не должен быть цифрой. Java не допускает никаких символов кроме
    $
    , а C# вообще не допускает никаких символов:

    int 7х; // неверно, цифра не может начинать идентификатор

    int х7; // верно, цифра может быть частью идентификатора

    int х; // верно

    int х$; // неверно, никакие символы недопустимы

    int @7k; // неверно, @ работает только для ключевых слов

    int @class; // верно, @ перед ключевым словом позволяет использовать

                // его в качестве идентификатора

    Стандарты именования

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

    camel
    , означающее, что методы и идентификаторы используют меленькую букву для первой буквы имени и заглавную букву для первой буквы любого другого слова в имени. Общий синтаксис, которому следуют большинство программистов в Java, представлен ниже:

    int id;

    int idName;

    int id_name; // также используется

    final int CONSTANT_NAME; // широко распространен

    int reallyLongId;


    public class ClassName; // каждая первая буква заглавная

    public interface _InterfaceName; // с предшествующим подчеркиванием


    public void method(){}

    public void methodName(){}

    public void longMethodName(){}

    public void reallyLongMethodName(){}

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

    I
    . Некоторые примеры приведены ниже:

    int id;

    int idName;


    public class ClassName // каждая первая буква заглавная

    public interface IInterfaceName // имени интерфейса предшествует I


    public void Method(){} // первая буква всегда заглавная

    public void MethodName(){} // первая буква всех других слов

                               // будет заглавная

    public void LongMethodName(){}

    public void ReallуLongMetodName(){}

    Ключевые слова

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

    int
    . Другими примерами ключевых слов являются
    public
    ,
    class
    ,
    static
    и
    void
    в листингах кода в этом приложении.

    Ключевые слова можно разделить на ряд категорий в связи с их назначением. В этом разделе мы выделим и определим каждую категорию, а также идентифицируем ключевые слова. Реальные ключевые слова будут идентифицироваться своими версиями в Java, чтобы можно было легко их находить. Затем будет дан эквивалент C# (если существует). Для тех ключевых слов, которые присутствуют только в Java, будет предоставлено лучшее соответствие. Ключевые слова, представленные в C#, но не в Java, будут даны в своей собственной категории с лучшим приблизительным эквивалентом в Java (если такой существует).

    Простейшие ключевые слова: byte, char, short, int, long, float, double и boolean

    Примитивные типы данных в обоих языках ссылаются на низкоуровневые типы значений языка. Конечно, диапазон значений указанных типов может различаться в том или другом языке. Логические значения в C# идентифицируются ключевым словом bool в противоположность boolean в Java. Ниже представлен табличный список типов данных Java и их аналогов в C#:

    Тип Java Описание Эквивалентный тип C# Описание
    byte
    8-битовое со знаком
    sbyte
    8-битовое со знаком
    short
    16-битовое со знаком
    short
    16-битовое со знаком
    int
    32-битовое со знаком
    int
    32-битовое со знаком
    long
    64-битовое со знаком
    long
    64-битовое со знаком
    float
    32-битовое число с плавающей точкой со знаком
    float
    32-битовое число с плавающей точкой со знаком
    double
    64-битовое число с плавающей точкой со знаком
    double
    64-битовое число с плавающей точкой со знаком
    boolean
    true/false
    bool
    true/false
    char
    2-байтовый Unicode char 2-байтовый Unicode

    Существует также ряд типов, поддерживаемых C#, которые Java не использует. Таблица ниже выделяет эти типы данных.

    Уникальный тип данных C# Описание
    Byte
    8-битовое целое без знака
    ushort
    16-битовое целое без знака
    Uint
    32-битовое целое без знака
    ulong
    64-битовое целое без знака
    decimal
    128-битовое
    Ключевые слова-переменные: this, void и super

    Эти ключевые слова сами являются переменными. Оба языка, Java и C#, имеют по три ключевых слова, которые попадают в эту категорию. Ключевые слова

    this
    и
    void
    обладают в обоих языках одинаковой функциональностью.

    super
    — эта ссылочная переменная используется для указания класса-предка. В C# эквивалентом является
    base
    . Возьмем класс
    Power
    , который предоставляет возможность найти степень заданного числа и степень, в которую требуется возвести (при условии, что не происходит переполнение):

    public class SuperEX {

     int power;

     public SuperEX(int power) {

      this.power = power;

     }

     public int aMethod(int x) {

      int total = 1;

      for (int i = 0; i < power; i++) {

       total *= x;

      }

      return total;

     }

     public static void main(String args[]) {

      SuperEX x = new SuperEX(Integer.parseInt(args[0]));

      int tot = x.aMethod(Integer.parseInt(args[1]));

      System.out.println(tot);

     }

    }

    Класс-потомок этого класса сможет получить доступ к методу

    aMethod
    с помощью вызова
    super.aMethod(<int value>)
    , к переменной
    power
    — с помощью вызова
    super.power = <int value>
    , и даже к конструктору — с помощью вызова
    super(<int value>)
    , где
    <int value>
    может быть любым целым литералом, переменной или константой.

    Аналогично в C# класс-потомок этого класса сможет получить доступ к методу

    aMethod
    с помощью вызова
    super.aMethod(<int value>)
    и к переменной
    power
    — с помощью вызова
    super.power = <int value>
    . Сделать вызов базового конструктора тоже возможно, синтаксис, однако, будет отличаться. Пример ниже является эквивалентом в C# для SuperEX:

    namespace SuperEX {

     using System;

     public class SuperEX {

      internal int power;

      public SuperEX(int power) {

       this.power = power;

      }

      public int aMethod(int x) {

       int total = 1;

       for (int i = 0; i < power; i++) {

        total *= x;

       }

       return total;

      }

      public static void Main(String [] args) {

       SuperEX x = new SuperEX(int.Parse(args[0]));

       int tot = x.aMethod(int.Parse(args[1]));

       Console.WriteLine(tot);

      }

     }

     public class Child: SuperEX {

      public Child() : base(55) { }

     }

    }

    Как можно видеть на примере класса-потомка

    Child
    , вызов конструктора базового класса является частью объявления конструктора класса-потомка. Программист может по своему усмотрению определить список параметров конструктора класса-потомка, но ссылка на конструктор базового класса должна соответствовать списку аргументов, требуемых базовым классом. В данном примере конструктор потомка может получить форму
    <child constructor>: base constructor(<int value>)
    , где
    <int value>
    может быть любым целым литералом, переменной или константой, a
    <child constructor>
    представляет любой конструктор потомка, который хочет воспользоваться конструктором базового класса. Более общая версия, как получить доступ к конструктору базового класса, представлена ниже:

    ChildConstructor(argument_list) : BaseConstructor(argument_list)

    Ключевые слова управления пакетами: import и package

    Так же как в Java, в C# инструкции

    import
    предоставляют доступ к пакетам и классам в коде без полной квалификации, директива
    using
    может использоваться для того, чтобы сделать компоненты пространства имен видимыми в классе без полной квалификации. В C# не существует эквивалента инструкции
    package
    . Чтобы сделать класс частью пространства имен, надо поместить его в объявление пространства имен. Пространства имен будут обсуждаться более подробно позже в этой главе.

    Ключевые слова управления потоком выполнения и итерациями: break, case, continue, default, do, else, for, if, instanceof, return, switch и while

    Большинство упомянутых выше ключевых слов имеют одинаковые имена, синтаксис и функциональность в C# и Java. Исключением является оператор Java

    instanceof
    , используемый для определения того, что объект является экземпляром класса или некоторого подкласса этого класса. C# предоставляет такую же функциональность с помощью ключевого слова
    is
    . Некоторые примеры того, как инструкции работают в C#, даны ниже. Можно видеть, что большая часть кода точно такая же, как и в Java:

    public static void Main (string[] args)

     int option = int.Parse(arg[0]);

     if (option == 1) {

      // что-нибудь сделать

     }

     else if (option == 2) {

      // сделать что-нибудь еще

     }

     switch (option) {

     case 1:

      // сделать что-нибудь

      break;

     case 2:

      // сделать что-нибудь еще

     default:

      break;

     }

    }

    C# вводит инструкцию

    foreach
    , используемую специально для перебора без изменения элементов коллекции или массива, чтобы получить желаемую информацию. Изменение содержимого может иметь непредсказуемые побочные эффекты. Инструкция
    foreach
    обычно имеет форму, показанную ниже:

    foreach(ItemType item in TargetCollection)

    ItemType
    представляет тип данных, хранящихся в коллекции или массиве, a
    TargetCollection
    представляет реальный массив или коллекцию. Существует два набора требований, которым должна удовлетворять коллекция, перебор элементов которой будет выполняться с помощью инструкции
    foreach
    . Первый набор имеет отношение к составу самой коллекции. Это следующие требования:

    □ Тип коллекции должен быть интерфейсом, классом или структурой.

    □ Тип коллекции должен включать метод

    GetEnumerator()
    для возврата типа перечислителя. Тип перечислителя является по сути объектом, который позволяет перебрать коллекцию элемент за элементом.

    Второй набор требований имеет дело с составом типа перечислителя, возвращаемого упомянутым выше методом

    GetEnumerator()
    . Список требований дан ниже:

    □ Перечислитель должен предоставить метод

    MoveNext()
    типа
    boolean
    .

    □ 

    MoveNext()
    должен возвращать
    true
    , если в коллекции есть еще элементы.

    □ 

    MoveNext()
    должен увеличивать счетчик элементов при каждом вызове.

    □ Тип перечислителя должен предоставлять свойство с именем

    Current
    , которое возвращает
    ItemType
    (или тип, который может быть преобразован в
    ItemType
    ).

    □ Метод доступа свойства должен возвращать текущий элемент коллекции.

    Следующий пример использует

    foreach
    для просмотра таблицы
    Hashtable
    :

    Hashtable t = new Hashtable();

    t["a"] = "hello";

    t["b"] = "world";

    t["c"] = "of";

    t["d"] = "c-sharp";

    foreach (DictionaryEntry b in t) {

     Console.WriteLine(b.Value);

    }

    Ключевые слова модификации доступа: private, protected, public, и (пакет по умолчанию)

    Ключевое слово

    private
    используется, чтобы сделать методы и переменные доступными только изнутри элементов содержащего их класса. Функциональность одинакова в обоих языках, модификатор
    public
    позволяет сущностям вне пакета получить доступ к внутренним элементам. Конечно, для C# это будут сущности вне пространства имен, а не пакета.

    C# и Java различаются в том, как обрабатываются

    protected
    и
    'default'
    . В то время как в Java
    protected
    делает метод или переменную доступной для классов в том же пакете или подклассах класса,
    protected
    в C# делает код видимым только для этого класса и подклассов, которые от него наследуют.

    C# вводит также новый модификатор доступа —

    internal
    . Ключевое слово
    internal
    изменяет члены данных так, что они будут видимы всему коду внутри компонента, но не клиентам этого компонента. Различие здесь между модификатором в Java, который указывает элемент, доступный только для элементов в пакете, и
    internal
    состоит в том, что
    internal
    доступен всем элементам сборки, которая может охватывать несколько пространств имен. Сборки и пространства имен будут рассмотрены позже в этом приложении.

    Модификаторы: abstract, class, extends, final, implements, interface, native, new static, synchronized, transient, volatile

    Модификатор

    abstract
    имеет одну и ту же форму и синтаксис в обоих языках. Таким же является ключевое слово
    class
    . C# не имеет модификаторов
    extends
    или
    implements
    . Чтобы вывести из класса или реализовать интерфейс, используйте оператор
    :
    . Когда список базового класса содержит базовый класс и интерфейсы, базовый класс следует первым в списке. Ключевое слово
    interface
    используется для объявления интерфейса. Примеры рассмотренных ранее концепций приведены ниже:

    class ClassA: BaseClass, Iface1, Iface2 {

     // члены класса

    }

    public interface IfruitHaver {

     public void Fruit();

    }

    public class plant: IfruitHaver {

     public Plant() {

     }

     public void Fruit() {

     }

    }

    class Tree : Plant {

     public Tree() {

     }

    }

    Ключевое слово

    final
    в Java трудно отнести к какой-то категории. Частично причина заключается в том, что оно предоставляет вид функциональности два-в-одном, что делает трудным соединение его с каким-либо одним назначением. Объявление класса как
    final
    запечатывает его, делая невозможным расширение. Объявление метода как
    final
    также его запечатывает, делая невозможным его переопределение. Объявление переменной как
    final
    является по сути объявлением ее только для чтения. Именно для чтения, а не константой, так как возможно задать значение
    final
    как значение переменной. Значения констант должны быть известны во время компиляции, поэтому константы могут задаваться равными только другим константам.

    В противоположность этому C# предоставляет специфические приемы для рассмотрения каждого отдельного вопроса. По умолчанию подкласс не должен иметь возможности повторно реализовывать открытый метод, который уже реализован в суперклассе. C# вводит новую концепцию — сокрытие метода, что позволяет программисту переопределить члены суперкласса в классе-наследнике и скрыть реализацию базового класса, C# использует в данном случае модификатор

    new
    .

    Это делается присоединением

    new
    к объявлению метода. Достоинство сокрытия версий членов базового класса состоит в том, что можно выборочно определить, какую реализацию использовать. Пример такой концепции представлен в коде ниже:

    namespace Fona {

     using System;

     public class Plant {

      public Plant(){}

      public void BearFruit() {

       Console.WriteLine("Generic plant fruit");

      }

     }

     class Tree : Plant {

      public Tree(){}

      // ключевое слово new используется здесь явно, чтобы скрыть

      // базовую версию

      new public void BearFruit() {

       Console.WriteLine("Tree fruit is:->Mango");

      }

     }

     public class PlantEX {

      public PlantEX(){}

      public static void Main(String[] args) {

       Plant p = new Plant();

       p.BearFruit(); // вызывает реализацию базового класса

       Tree t = new Tree();

       t.BearFruit(); // вызывает реализацию класса наследника

       ((Plant)t).BearFruit(); // вызывает реализацию базового класса,

                               // используя наследника.

      }

     }

    }

    Выполнение этого примера создает следующий вывод:

    Generic plant fruit

    Tree fruit is:->Mango

    Generic plant fruit

    Необходимо отметить, что существует различие между сокрытием метода и обыкновенным полиморфизмом. Полиморфизм всегда будет предоставлять для вызова метод класса-наследника.

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

    new
    не было использовано.

    Чтобы предоставить функциональность переопределения, используются модификаторы

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

    class Tree Plant {

     public Tree() {}

     public override void Fruit() {

      Console.WriteLine("Tree fruit is:->Mango");

     }

    }

    Компиляция и выполнение этого создают следующий вывод.

    Generic plant fruit

    Tree fruit is:->Mango

    Tree fruit is:->Mango

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

    Fruit()
    независимо от использования cтратегии преобразования
    ((Plant)t).BearFruit()
    , которая применялась ранее для ссылки на метод
    Fruit()
    базового класса. Модификатор new может также использоваться для сокрытия любого другого типа наследованных из базового класса членов аналогичной сигнатуры.

    Чтобы помешать случайному наследованию класса, используется ключевое слово

    sealed
    . В приведенном выше призере можно изменить объявление
    Plant
    на
    public sealed class Plant
    и в этом случае
    Tree
    больше не сможет от него наследовать.

    C# не имеет модификатора

    native
    . В Java использование
    native
    указывает, что метод реализован на зависимом от платформы языке. Это требует чтобы метод был абстрактным, так как реализация должна находиться в другом месте. Ближайшим к этому типу функциональности является модификатор
    extern
    . Использование
    extern
    предполагает, что код реализуется вовне (например, некоторой собственной DLL). Однако в отличие от Java, нет необходимости использовать ключевое слово
    abstract
    в соединении с ним. Фактически это приведет к ошибке, так как они означают две похожие, но различные вещи. Ниже класс
    Plant
    из предыдущего примера показывает, как можно использовать
    extern
    :

    public class Plant : IfruitHaver {

     public extern int See();

     public Plant(){}

     public void Fruit() {

      Console.WriteLine("Generic plant fruit");

     }

    }

    Это не имеет большого смысла без использования атрибута

    DllImport
    для определения внешней реализации. Более подробно атрибуты будут рассмотрены позже в приложении. Дальнейший код делает соответствующие изменения, предполагая, что функция
    See
    экспортирована ресурсом
    User32.dll
    :

    public class Plant: IfruitHaver {

     [System.Runtime.InteropServices.DllImport("User32.dll)]

     public static extern int See();

     public Plant(){}

     public void Fruit() {

      Console.WriteLine("Generic plant fruit");

     }

    }

    Здесь метод

    See()
    помечен как статический. Атрибут
    DllImport
    требует этого от методов, на которых он используется

    Пока не существует версии C# ключевых слов

    transient
    ,
    volatile
    или
    synchronized
    . Однако существует ряд способов, предоставляемых SDK .NET для имитации некоторой их функциональности. C# использует атрибут
    NonSerialized
    , связанный с полями класса, для предоставления механизма, аналогичного по функциональности модификатору Java
    transient
    , этот атрибут однако опротестован, поэтому может быть изменен в будущих версиях.

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

    Monitors
    . Они предоставляют возможность делать и освобождать блокировки синхронизации на объектах
    SyncBlocks
    , которые содержат блокировку используемую для реализации синхронизированных методов и блоков кода; список ожидающих потоков выполнения, используемых для реализации функциональности монитора
    ReaderWriterLock
    , который определяет образец одного писателя для многочисленных читателей; примитивы синхронизации
    Mutex
    , предоставляющие межпроцессную синхронизацию; и
    System.Threading.Interlocked
    , который может использоваться для предоставлении синхронизированного доступа к переменным через несколько потоков выполнения.

    Первый шаг к синхронизации в C# состоит в ссылке на сборку

    System.EnterpriseServices.dll
    . Инструкция
    lock(<expression>) {// блок кода}
    является единственным, связанным с синхронизацией, ключевым словом в C#. Оно может применяться, также как в Java, для получения взаимно исключающего доступа к блокировке объекта
    <ref>
    . Все попытки получить доступ к
    <expression>
    будут блокированы, пока поток выполнения с блокировкой не освободит ее. Обычно используется выражение либо
    this
    , либо
    System.Type
    , соответствующее
    Type
    представленного объекта. Их использование будет защищать переменные экземпляра выражения в то время, как использование
    System.Type
    будет защищать статические переменные.

    Ключевые слова обработки ошибок: catch, finally, throw, throws, try

    Эти модификаторы являются одинаковыми в обоих языках, за исключением инструкции

    throws
    , которая отсутствует в C#. Пугающая вещь в отношении инструкции
    throws
    из Java состоит в том, что она позволяет потребителям компонента с помощью относительно простого синтаксиса использовать компонент, не зная какие исключения он может порождать. Можно удовлетвориться заверениями, что компилированный код обрабатывает все, имеющие отношение к делу, исключения, так как компилятор будет иначе отказывать и информировать обо всех не перехваченных исключениях. Функциональность такого рода отсутствует в C# в настоящее время. Предоставление метода потребителям сборки, желающем знать, когда порождаются исключения, должно будет привести к хорошей практике документирования или некоторому умелому программированию атрибутов.

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

    // OverflowEX.java

    public class OverfTowEX {

     publiс static void main(String args []) {

      byte x = 0;

      for (int i = 0; i < 130; i++) {

       x++;

       System.out.println(x);

      }

     }

    }

    Как известно,

    byte
    в Java является 8-битовым типом данных со знаком. Это означает, что диапазон значений
    byte
    лежит от -128 до 128. Результатом добавления единицы к любой границе заданного диапазона целого типа будет другая граница диапазона целого типа. Поэтому в этом примере добавление 1 к 127 создаст -128. И если откомпилировать и выполнить эту программу, то последние пять чисел выведенные на консоли будут следующими:

    126

    127

    -128

    -127

    -126

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

    □ Программный подход. Чтобы бороться с этим типом молчаливых ошибок, C# вводит концепцию проверяемых и непроверяемых инструкций. Ключевое слово

    checked
    используется для управления контекстом проверки переполнения в случае операций и преобразований арифметики целого типа, таких, как представленные выше. Оно может использоваться как оператор или как инструкция. Инструкции
    checked/unchecked
    следуют приведенному ниже синтаксису (
    i
    ). Они предназначены для помещения в скобки ряда инструкций, которые могут создавать переполнение. Синтаксис операции
    checked/unchecked
    показан в пункте (
    ii
    ). Операция checked проверяет переполнение одиночных выражений:

    (i) checked {block_of_code}

    unchecked { block_of_code}


    (ii) checked (expression)

    unchecked (expression)

    block_of_code
    содержит код, в котором инструкция
    checked/unchecked
    наблюдает за переполнением, a
    expression
    представляет выражение, в котором
    checked/unchecked
    наблюдает за переполнением в конечном значении. Показанный далее пример иллюстрирует использование
    checked/unchecked
    :

    // OverflowEX.cs

    public class OverflowEX {

     public static void Main(String() args) {

      sbyte x = 0; // помните, что необходимо изменить byte на sbyte

      for (int i = 0; i < 130; i++) {

       checked {

        // можно также использовать checked(x++)

        x++;

        Console.WriteLine(x);

       }

      }

     }

    }

    □ Подход с ключом компилятора. Для контроля переполнения во всем приложении может использоваться настройка компилятора

    /checked+
    . Чтобы проиллюстрировать это, удалим инструкцию
    checked
    из приведенного выше примера и попытаемся компилировать его, используя флаг
    /checked+
    . С его помощью можно включать и выключать проверку арифметического переполнения, изменяя состояние конфигурационных свойств на странице свойств проекта. Задание значения как true будет включать проверку переполнения.

    При включенной проверке переполнения можно обсудить использование инструкции

    unchecked
    . По сути она предоставляет функциональность для произвольного исключения проверки выражений или блоков инструкций в то время, когда включена проверка во всем приложении. В примере ниже предыдущая инструкция заменяется инструкцией
    unchecked
    . Компиляция и выполнение этого кода будет создавать вывод, аналогичный выводу
    OverflowEX.java
    .

    // OverflowEX.сs.

    public class OverflowEX {

     public static void Main(String[] args) {

      sbyte X = 0;

      for (int i = 0; i < 130; i++) {

       unchecked { // можно также использовать unchecked(x++)

        x++;

        Console.WriteLine(x);

       }

      }

     }

    }

    Входные и выходные данные

    Возможность собрать входные данные из командной строки и вывести данные в командной строке является интегральной частью функциональности ввода/вывода в Java. Обычно в Java необходимо создать экземпляр объекта

    java.io.BufferedReader
    , используя поле
    System.in
    , чтобы извлечь ввод из командной строки. Ниже представлен простой класс Java —
    JavaEcho
    , который получает ввод с консоли и выводит его обратно, чтобы проиллюстрировать использование пакета
    Java.io
    для сбора и форматирования ввода и вывода:

    // JavaEcho.java

    import java.io.*;

    public class JavaEcho {

     public static void main(String[] args) throws IOException {

      BufferedReader stdin = new BufferedReader(new InputSreamReader(System.in));

      String userInput = stdin.readLine();

      System.out.println("You said: " + userInput);

     }

    }

    Класс

    System.Console
    предоставляет методы в C#, которые дают аналогичную функциональность для чтения и записи из и в командную строку, Нет необходимости в каких-либо дополнительных объектах, класс
    Console
    предоставляет методы, которые могут читать целые строки, читать символ за символом и даже показывать описанный выше поток, из которого выполняется чтение. Важно отметить, что эту функциональность дает
    System.Console
    без создания экземпляра объекта
    Console
    . Фактически можно обнаружить, что невозможно создать экземпляр объекта
    Console
    . Члены класса
    Console
    кратко описаны в таблицах ниже:

    Открытые статические свойства (общие) Описание
    Error
    Получает стандартный выходной поток ошибок системы
    In
    Получает стандартный входной поток ошибок системы
    Out
    Получает стандартный поток вывода системы
    Открытые статические методы (общие) Описание
    OpenStandardError
    Перезагруженный. Возвращает стандартный поток ошибок.
    OpenStandardInput
    Перезагруженный. Возвращает стандартный поток ввода.
    OpenStandardOutput
    Перезагруженный. Возвращает стандартный поток вывода.
    Read
    Читает следующий символ из стандартного потока ввода.
    ReadLine
    Читает следующую строку символов из
    Console.In
    , который по умолчанию задается как стандартный поток ввода системы.
    SetError
    Перенаправляет свойство
    Error
    для использования указанного потока
    TextWriter
    .
    SetIn
    Перенаправляет свойство
    In
    для использования указанного потока
    TextReader
    .
    SetOut
    Перенаправляет свойство
    Out
    для использования указанного потока
    TextWriter
    .
    Write
    Перезагруженный. Записывает указанную информацию в
    Console.Out
    .
    WriteLine
    Перезагруженный. Записывает информацию, за которой следует конец строки в
    Console.Out
    .

    Как можно видеть, все члены

    Console
    являются статическими.
    static
    является примером модификатора C#. Он обладает тем же значением, что и его аналог в Java, т.е. делает указанную переменную или метод принадлежащим всему классу, а не одному какому-то экземпляру класса. Мы обсудим модификаторы более подробно позже в этом приложении.

    С помощью мощных методов из класса

    Console
    можно записать эквивалент класса
    JavaEcho
    на C# следующим образом:

    class CSEchoer {

     static void Main(string[] args) {

      string userInput = System.Console.ReadLine();

      System.Console.WriteLine("You said : " + userInput);

     }

    }

    Приведенный выше код значительно короче и легче, чем его аналог на Java. Статический метод

    Console.WriteLine
    предоставляет одну полезную вещь, а именно, возможность использовать форматированные строки. Гибкость форматированных строк может быть проиллюстрирована написанием простой игры, где ввод пользователя применяется для создания рассказа. Код
    EchoGame
    представлен ниже:

    class EchoGame {

     static void Main(string[] args) {

      System.Console.WriteLine("Once upon a time in a far away" + "?");

      string userInput1 = System.Console.ReadLine();

      System.Console.WriteLine("a young prince ?");

      string userInput2 = System.Console.ReadLine();

      System.Console.WriteLine("One day while?");

      string userInput3 ? System.Console.ReadLine();

      System.Console.WriteLine("He came across a ?");

      string userInput4 = System.ConsoleReadLine();

      System.Console.WriteLine("The prince ?");

      String userInput5 = System.Console.ReadLine();

      System.Console.WriteLine("Once upon a time in a far away"

       + " {0}, a young prince {1}. \n One day"

       + "while {2}, He came across a (3). \n The "

       + "prince {4} ! ", userInput1, userInput2,

       userInput3, userInput4, userInput5);

     }

    }

    Точки вставки заменяются предоставленными аргументами, начиная с индекса

    {0}
    , который соответствует самой левой переменной (в данном случае
    userInput1
    ). Можно подставлять не только строковые переменные, и не обязательно использовать только переменные или переменные одного типа. Любой тип данных, который может вывести метод
    WriteLine
    , допустим в качестве аргумента, включая строковые литералы или реальные значения. Не существует также ограничений на число точек вставки, которые могут добавляться к строке, пока их не больше общего числа аргументов. Отметим, что исключение точек вставки из строки приведет к тому, что переменные не будут выводиться. Необходимо, однако, иметь аргумент для каждой определенной точки вставки, индекс которой в списке аргументов соответствует индексу точки вставки. В следующем листинге, например, удаление
    {1}
    допустимо, пока имеется три аргумента. В этом случае
    {0}
    соответствует
    strA
    и
    {2}
    соответствует
    strC
    :

    Console.WriteLine("hello {0} {1} {2}", strA, strB, strC);

    Компиляция

    При описании некоторых различий между JRE Java и CLR C# кратко упоминались некоторые детали того, как написанный на соответствующем языке код компилируется и выполняется. Хотя код обоих языков компилируется в некоторую промежуточную форму, байт-код версии Java никогда не компилируется повторно в собственные инструкции машины (если только не используется компилятор в собственный код). Вместо этого байт-код требует для выполнения среду времени выполнения, и в частности, виртуальную машину. Имя компилированного файла связано с именем файла, в котором находится исходный код, который в свою очередь связан с именем открытого класса в этом файле. В случае определения нескольких классов в одном файле каждое определение класса будет создавать файл класса, который соответствует имени определенного класса. Например, возьмем исходный файл

    Test.java
    со следующим кодом:

    // Test.java

    class x {}

    class у {}

    class z {}

    Компиляция этого файла будет создавать три файла классов:

    х.class
    ,
    у.class
    ,
    z.class
    . Один (и только один для каждого исходного файла) из этих классов может быть объявлен открытым следующим образом:

    // Test.java

    public class x {}

    class у {}

    class z {}

    В приведенном выше примере имя исходного файла

    Test.java
    должно быть изменено, чтобы соответствовать имени находящегося в нем открытого класса.
    Test.java
    поэтому станет
    х.java
    , чтобы код компилировался.

    В противоположность этому, компилированный в IL код C# выполняется VES (Virtual Execution System), которая предоставляет поддержку для IL, загружая управляемый код, и JITters (которые преобразуют управляемый код в форме промежуточного языка в собственный код машины). Имя файла

    Hello.cs
    не связано с именем конечного исполняемого файла и может изменяться во время компиляции с помощью ключа
    /out
    . Если имя файла вывода не определено, то exe получит имя того файла исходного кода, который содержит метод
    main
    , a DLL получит имя первого указанного файла исходного кода. Фактически имя файла даже не связано с определениями любых классов, находящихся в файле. Класс
    Hello
    может быть определен в файле
    Goodbу.cs
    , который компилируется в несвязанный
    MisterHanky.exe
    .

    SDK .NET поставляется вместе с компилятором, поэтому не нужно беспокоиться о получении специальных компиляторов для этого приложения. Откройте просто командную строку, перейдите в каталог, где сохранен файл

    hello.cs
    , и введите:

    csc hello.cs

    Файл будет компилирован в

    hello.exe
    . Хотя большинство программистов Java знакомы с этой формой низкоуровневой компиляции, важно заметить, что Visual Studio.NET предоставляет аналогичную функциональность, интегрированную в IDE. Например, изменение имени исполнимого файла можно легко выполнить, добавляя свойство имени сборки страницы свойств проекта. Чтобы сделать это производим щелчок правой кнопкой мыши на имени проекта в панели Solution Explorer и выбираем Properties или указываем Project в меню, в то время, когда имя проекта выделено, и выбираем пункт меню Properties. В папке Common Properties можно выбрать страницу General Properties и изменить свойство Assembly Name. Можно будет увидеть, что свойство для чтения Output File изменяется, отражая новое имя сборки.

    Типы компиляции

    Все файлы Java компилируются в файл байт-кода с расширением

    .class
    , который может выполниться виртуальной машиной. Внутри первого исходного кода необходимо предоставить соответствующую. Функциональность для создания того или иного типа приложения. Например, определенный ниже код будет создавать окно, аналогичное
    Form
    в Windows. Второй исходный файл
    AddLib.java
    является вспомогательным классом, используемым для выполнения сложения двух целых чисел. Можно заметить, что они включены в отдельные пакеты и
    JavaFrame
    импортирует класс
    AddLib
    . Пакеты и их эквивалент C# будут рассмотрены в следующем разделе:

    // код для JavaFrame.java

    Package com.javaapp;

    import java.awt.*;

    import java.io.*;

    import com.javalib.AddLib;


    public class JavaFrame extends java.awt.Frame {

     public static void main (String[] args) {

      JavaFrame jfrm = new JavaFrame();

      jfrm.setSize(100, 100);

      jfrm.setVisible(true);

      AddLib lib = new AddLib();

      jfrm setTitle("Frame Version " + lib.operationAdd(12, 23));

     }

    }


    // код для AddLib.java

    Package com.javalib;

    public class AddLib {

     public AddLib() {

     }

     public int operationAdd(int a, int b) {

      return a + b;

     }

    }

    Java предоставляет двухшаговый процесс для создания исполнимого файла или библиотечных единиц компиляции: откомпилировать файл(ы) и сделать файл доступным, предоставляя компилятору путь доступа к папке, где находятся файлы (можно написать пакетный файл, который будет делать это за один шаг). Класс можно сделать доступным с помощью ключа компилятора

    -classpath
    для определения, где компилятор может искать разрешение символов, не определенных в исходном коде (различные компиляторы могут иметь различные имена для этого ключа). Система имеет также переменную среды окружения
    Classpath
    . Если ключ
    -classpath
    не определен, то компилятор будет искать переменную окружения, если ключ
    -classpath
    определен, то он переопределяет любые записи
    Classpath
    , которые могут существовать среди переменных окружения для этой специфической компиляции.

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

    JavaFrame
    и
    AddLib
    необходимо будет откомпилировать в файлы классов. Путь доступа к этим файлам классов можно затем добавить к переменной окружения
    CLASSPATH
    . Когда путь доступа к классам задан, любые классы из пакета могут выполняться из любого каталога в системе, передавая полностью квалифицированное имя класса в виртуальную машину. Вызов с помощью SUN Microsystems JDK 1.3:

    java javalib.JavaFrame

    приводит к выполнению программы

    JavaFrame
    и созданию
    Frame
    с панелью заголовка, выводящей Java Frame Version 35.

    Код в C# всегда после компиляции автоматически объединяется в тот или иной тип компонента. Единицы компиляции могут содержать столько файлов и определений классов, сколько потребуется. Способ использования этой функциональности снова делится между использованием командной строки и использованием IDE (в частности, Visual Studio.NET). Создание нового проекта с помощью VS.NET требует, чтобы был определен тип создаваемого проекта. Среди других имеются возможности создать консольное приложение, оконное приложение и библиотеку классов. Можно создать даже пустой проект и определить тип вывода позже. Процесс определения типа вывода проекта с помощью VS.NET описан дальше в этом разделе. В командной строке для изменения типа вывода используется ключ

    /target: <target-type>
    , где
    target-type
    является одной из строк:

    Ехе

    Library

    Winexe

    Можно добавить любое число файлов как аргументы, разделенные пробелами:

    csc /target:<target-type> <file1> <file2> ... <filen>

    Добавление нескольких файлов в единицу компиляции с помощью VS.NET является вопросом добавления отдельных файлов в проект. Можно легко изменить тип выходного файла, поменяв свойство Output Type на странице General Properties (смотрите выше детали доступа к странице с общими свойствами).

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

    Цель данного изложения состоит в создании версии C# файлов исходного кода

    JavaFrame
    и
    AddLib
    и рассмотрении деталей процесса создания кода C#. Так как два эти класса используют пакеты и импортирование, необходимо обсудить их эквиваленты в C#.

    Классы Java могут располагаться в логических подразделениях, называемых пакетами. Пакет определяется как сущность, которая группирует классы вместе. Пакеты могут облегчить импорт другого кода программиста и более важно определить ограничения доступа к переменным и методам.

    Пространства имен в C# предоставляют аналогичный механизм для объединения управляемых классов, но являются значительно более мощными и гибкими. Здесь речь идет об управляемых классах, а не специальных классах C#, так как классы в пространстве имен могут быть из любого, соответствующего CLS, языка (вспомните, что CLS является независимым от языка). Пакеты и пространства имен, однако, существенно различаются своей реализацией. Класс Java, который необходимо, например, сделать частью пакета

    com.samples
    , должен иметь в качестве первой строки кода в файле
    Package com.samples
    . Это, конечно, исключает какие-либо комментарии. Любой код внутри этого файла автоматически становится частью указанного пакета. Имя пакета в Java также связывается с папкой, содержащей файл класса в том смысле, что они должны иметь одинаковые имена. Пакет
    com.samples
    поэтому должен находиться в файле, который существует в папке com\samples. Давайте рассмотрим некоторые примеры того, как работают пакеты.

    // package_samples.java

    package samples.on; // отображает прямо в папку, где находится файл класса

    public class Packaging {

     int x;

     public class Internal {

      // находится автоматически в том же пакете

     }

     public static void main(String args[]) {

     }

    }

    class Internal {

     // находится автоматически в том же пакете

    }

    Примеры того, как этот код может выполняться, приведены ниже. Это, конечно, предполагает, что файл класса был сделан доступным для JRE:

    □ Из командной строки:

    java samples.on.Packaging

    □ Как непосредственная ссылка в коде:

    // Referencer.java

    public class Referencer {

     samples.on.Packaging pack = new samples.on.two.three.Packaging();

    □ Используя директиву

    import
    , можно опустить полностью квалифицированные имена пакетов, поэтому
    Referencer
    запишется как:

    // Referencer.java

    import samples.on.*;

    public class Referencer{

     Packaging pack = new Packaging();

    }

    Помещение класса в пространство имен достигается в C# с помощью ключевого слова

    namespace
    с идентификатором и заключением целевого класса в скобки. Вот пример:

    // namespace_samples.cs

    namespace Samples.On {

     using System;

     public class Example {

      public Example() {

      }

     }

    }

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

    // namespace_samples.cs

    namespace Samples.On {

     using System;

     public class Example {

      public Example() {

      }

     }

    }

    namespace Com.Cslib {

     using System;

     using System.Collections;

     public class AddLib {

      public AddLib() {

      }

      public int operationAdd(int a, int b) {

       return a + b;

      }

     }

    }

    Пространства имен вводятся с помощью директивы

    using <namespace name>
    , где
    <namespace name>
    является именем пространства имен. В C# не требуется использовать
    *
    , так как директива
    using
    неявно импортирует все элементы указанного пространства имен. Другим преимуществом является то, что пространства имен могут быть добавлены исключительно в конкретный класс. Хотя классы
    Example
    и
    AddLib
    выше определены в файле
    namespace_samples.cs
    .
    Example
    не имеет доступа к пространству имен
    System.Collections
    , несмотря на то, что
    AddLib
    его имеет. Однако инструкция
    import
    из Java не является специфической для класса. Она импортирует указанные элементы в файл. Вновь обратимся к
    х.java
    .

    // х.java

    public class x {

    }

    class у {

    }

    class z {

    }

    Если добавить инструкцию импорта, такую как

    import java.util.Hashtable
    , все классы, определенные внутри этого файла, будут иметь доступ к классу
    Hashtable
    . Код ниже будет компилироваться:

    // x.java

    package samples;

    import java.util.Hashtable;

    public class x {

     Hashtable hash = new Hashtable();

    }

    class у {

     Hashtable hash = new Hashtable();

    }

    class z {

     Hashtable hash = new Hashtable();

    }

    Пространства имен можно также определять внутри другого пространства имен. Этот тип гибкости недоступен в Java без создания подкаталогов. Приведенное выше пространство

    Com.Cslib
    можно расширить следующим образом:

    namespace Com.Cslib {

     using System;

     public class AddLib {

      public AddLib() {

      }

      public int operationAdd(int a, int b) {

       return a + b;

      }

     }

     namespace Ext {

      public class AddLib {

       public AddLib() {

       }

       public int operationAdd(int a, int b) {

        return a + b;

       }

      }

     }

    }

    Пакет Java

    com.javalib
    можно расширить, чтобы отобразить приведенный выше код, создав новую папку
    \EXT
    в каталоге
    com\javalib
    . В этой папке создается файл исходного кода
    AddLib.java
    следующим образом:

    package com.javalib.ext;

    public class AddLib {

     public AddLib() {

     }

     public int operationAdd(int a, int b) {

      return a + b;

     }

    }

    Отметим, что имя пакета было расширено для этого класса до

    com.javalib.ext
    .

    Внутреннее пространство имен и подпакеты доступны с помощью оператора точки "."; следовательно, можно было в C# извлечь расширенный AddLib с помощью нотации

    Com.Cslib.Ext.AddLib
    . В Java можно было бы использовать
    com.javalib.ext.AddLib
    .

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

    Com.Cslib.AddLib
    является не тем же классом, что и
    Com.Cslib.Ext.AddLib
    .

    Классы Java являются частью пакета, нравится им это или нет. Все классы, созданные без указания пакета, предполагают включение в пакет по умолчанию. C# имитирует эту функциональность. Даже если не объявить пространство имен, оно будет создано по умолчанию. Оно присутствует в каждом файле и доступно для использования в именованных пространствах имен. Так же как в Java нельзя изменить информацию о пакете, пространства имен нельзя модифицировать. Пакеты могут охватывать несколько файлов в одной папке, пространство имен может охватывать несколько файлов в любом числе папок и даже в нескольких сборках (сборки будут рассмотрены в следующем разделе). Два класса, охватываемые пространством имен А, которые определены в отдельных файлах и существуют в отдельных папках, оба являются частью пространства имен А.

    Чтобы получить доступ к элементу в пространстве имен, необходимо либо использовать полностью квалифицированное имя типа (в приведенном выше примере это

    Com.Cslib.AddLib
    ) или импортировать элемент пространства имен в текущее пространство имен, используя директиву
    using
    . Отметим, что по умолчанию доступность типов данных внутри пространства имен является внутренней. Необходимо явно отметить типы данных как открытые (
    public
    ), если требуется сделать их доступными без полной квалификации, но придерживаться такой стратегии строго не рекомендуется. Никакие другие модификаторы доступа не разрешены. В Java внутренние типы пакета могут также помечаться как
    final
    или
    abstract
    , или не помечаться вообще (этот доступ по умолчанию открывает их только для потребителей внутри пакета). Модификаторы доступа будут рассматриваться позже в этом приложении.

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

    using
    . Алиасы
    using
    существенно облегчают квалификацию идентификатора для пространства имен или класса. Синтаксис очень простой. Предположим, что имеется пространство имен
    Very.Very.Long.Namespace.Name
    . Можно определить и использовать алиас
    using
    для пространства имен следующим образом:

    using WLNN = Very.Very.Long.Namespace.Name;

    Конечно, имя псевдонима (алиаса) является произвольным, но должно следовать правилам именования переменных в C#.

    Создание и добавление библиотек при компиляции

    Ранее, при обсуждении компиляции и единиц компиляции кратко было сказано о концепции библиотек. Если создана библиотека, то необходимо, чтобы она была доступна для всех потенциальных потребителей. В Java это делается добавлением пути доступа к папке, содержащей классы библиотеки, в переменную окружения

    Classpath
    . Конечно, чтобы упростить это, можно добавить файлы класса в папке в JAR и поместить путь доступа к файлу
    jar
    в
    Classpath
    . В любом случае работа загрузчика классов состоит в нахождении всех неразрешенных ссылок, и он будет искать их в
    Classpath
    .

    C# предоставляет совсем другие механизмы для упаковки классов в библиотеку. По умолчанию все файлы C# в проекте станут частью единицы компиляции при использовании VS.NET. Если применяется командная строка, необходимо будет явно добавлять каждый файл, который должен быть частью единицы компиляции, как описано выше.

    Библиотеки кодов компилируются в РЕ (portable executable — переносимый исполнимый) тип файла. Необходимо в этом месте сделать различие между библиотеками кодов, о которых идет речь, и проектом Class Library в C#, который может создаваться с помощью IDE VS.NET. Под библиотекой кода понимается повторно используемое множество файлов C#, соединенных вместе в некоторой единице компиляции, поэтому происходит ссылка на файл РЕ, а не на реальную DLL или ЕХЕ. Библиотека кода такого вида более часто называется сборкой, поэтому это название будем использовать в дальнейшем, чтобы избежать путаницы.

    Так же как файл JAR, сборка имеет манифест, который описывает ее содержимое. Манифест сборки C# делает, однако, значительно больше. Он содержит все метаданные (совокупность данных, описывающая, как связаны элементы сборки), необходимые для определения требований версии, идентичности безопасности и всей информации, служащей для определения области действия сборки и разрешения ссылок на ресурсы и классы. Манифест сборки может храниться либо в файле РЕ (ЕХЕ или DLL) с кодом IL, либо как автономный файл, который содержит только данные манифеста сборки. В .NET пространства имен, содержащиеся внутри сборки, представлены внешнему миру (то есть, любым потребителям сборки, таким, как другие сборки) с помощью информации метаданных о типах, хранящейся в манифесте сборки.

    Классы в

    namespace_samples.cs
    могут компилироваться в библиотеку с помощью команды:

    CSC /target:library /out:FirstLibrary.dll namespace_samples.cs

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

    /addmodule
    и
    /reference
    . Они являются по сути одинаковыми, за исключением того, что
    /reference
    предназначен для сборок с манифестом сборки, в то время как
    /addmodule
    предназначен для сборок без манифеста сборки (модуль не имеет манифеста сборки). Синтаксис для добавления внешних ссылок из командной строки будет следующим:

    csc /reference: <lib.dll>; <libn.cs> <filename.exe>

    или:

    csc /addmodule: <lib.dll>; <libn.cs> <filename.exe>

    Чтобы добиться этого с помощью VS.NET, сделайте щелчок правой кнопкой мыши на папке References своего проекта в Solution Explorer и выберите Add Reference. Появится диалоговое окно, содержащее ряд доступных ссылок и позволяющее просматривать файловую систему в поисках ссылок. Можно также вызвать это диалоговое окно, выбирая пункт Add Reference из меню Project, когда выделен требуемый проект. Добавление ссылки в текущую сборку обычно копирует упомянутый файл в папку проекта, где будет располагаться новая сборка.

    Давайте создадим теперь потребителя для библиотеки

    AddLib
    , который аналогичен по форме и функциональности созданному ранее классу
    JavaFrame
    :

    // LibConsumer.cs

    namespace Com.CSapp {

     using System;

     using System.Windows.Forms;

     using Com.CsLib;

     public class Form1 : System.Windows.Forms.Form {

      public Form1() {

       AddLib al = new AddLib();

       this.Text = "C# Form version " + al.operationAdd(12, 23);

      }

      public override void Dispose() {

       base.Dispose();

      }

      Static void Main(string[] args) {

       Form1 f1 = new Form1();

       Application.Run(f1);

      }

     }

    }

    Этот код можно компилировать из командной строки со ссылкой на

    FirstLibrary.dll
    , введя следующую команду:

    csc /reference: FirstLibrary.dll /target:exe /out:EmptyForm.exe LibConsumer.cs

    Используя VS.NET, надо сначала сослаться на

    FirstLibrary.dll
    , как описано ранее, и затем собрать приложение. Когда приложение будет успешно собрано, выполните его. Оно должно создать форму Windows с заголовком C# form Version 35.

    Обнаружение и разрешение

    Мы уже обсудили, как JRE разрешает ссылки на другие классы, используя загрузчик классов для проверки во время выполнения переменной окружения Classpath. CLR проходит также ряд шагов, часто называемых зондированием (probing), при попытке найти сборку и разрешить ссылку на сборку. Попытка выполнить

    EmptyForm.exe
    приводит в движение ряд вещей. По умолчанию весь управляемый код загружается в домен приложения и обрабатывается определенным потоком выполнения операционной системы. Указанные сборки, означающие, что их типы используются в коде домена приложения, также должны загружаться, прежде чем их можно будет выполнить. CLR проходит несколько стадий, чтобы обнаружить и связаться с указанной сборкой.

    Так как поведение по соединению со сборками может конфигурироваться на основе конфигурационных файла приложения, файла издателя и файла машины/администратора, CLR должна брать эту конфигурационную информацию, чтобы обеспечить извлечение соответствующей версии указанной сборки. Все эти файлы основаны на XML и следуют похожему синтаксису. Они предоставляют такую информацию, как перенаправление связывания, расположение кода и режимы связывания для определенных сборок. Обычно правильная версия сборки определяется некоторой комбинацией трех конфигурационных файлов и собственным манифестом сборки.

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

    admin.cfg
    , чтобы определить ограничения связывания, локальные для данной машины.

    Затем CLR необходимо найти сборку. В Java JRE будет действовать в текущем каталоге и в

    classpass
    , чтобы найти класс, необходимый для разрешения ссылки. В определении расположения заданной сборки CLR полагается на элемент
    <codeBase>
    , связанный с упомянутыми выше конфигурационными файлами. Если такой элемент не предоставлен, то CLR ищет файл (называемый probing) в корневом каталоге приложения, во всех каталогах, перечисленных в конфигурационных файлах элемента
    <probing>
    и во всех подкаталогах корня приложения, которые имеют такое же имя, как и зондируемая сборка. Эти элементы всегда определяются относительно корневого каталога приложения. Отметим, что CLR всегда ищет имя сборки, соединенное с двумя допустимыми расширениями файлов РЕ —
    .exe
    и
    .dll
    .

    Наконец, если ссылка сделана на сборку с устойчивым именем, CLR будет искать в глобальном кэше сборок. Устойчивые имена и глобальный кэш сборок мы рассмотрим более подробно в следующем разделе.

    Строгие имена и глобальный кэш

    Устойчивое (строгое) имя делает сборку глобально уникальной. Оно состоит из идентификатора сборки, открытого ключа и цифровой подписи. Так как сборка, созданная с одним открытым ключом имеет другое имя, чем сборка, созданная с другим открытым ключом, то уникальность имени сборки гарантирована. Нельзя изменить сборку с устойчивым именем, не имея доступа к открытому ключу, использованному при ее создании, следовательно, устойчивое имя гарантирует, что никто не сможет создать другие версии этой сборки. Устойчивые имена предоставляют также жесткий контроль целостности. Создание устойчивого имени является двухшаговым процессом. Сначала необходимо создать или иметь ключевую пару. Создание ключевой пары можно выполнить в командной строке с помощью команды:

    sn -k <имя файла ключа>

    Например, чтобы создать файл ключа

    examplekey.key
    , надо ввести:

    sn -k examplekey.key

    После создания файл ключа используется для присвоения устойчивого имени сборке двумя способами. Из командной строки можно использовать утилиту

    alink
    следующим образом:

    al /keyfile: <имя файла ключа> <имя сборки>

    Чтобы задать для

    FirstLibrary.dll
    строгое имя, можно выполнить:

    al /keyfile:examplekey.key FiretLibrary.dll

    или в VS.NET изменить атрибут

    [assembly: AssemblyKeyFile("")]
    в файле
    AssemblyInfo.cs
    . Замените просто пустую строку на строку, представляющую имя файла ключа. Сборку
    FirstLibrary.dll
    можно затем ассоциировать с ключом, меняя атрибут
    AssemblyKeyFile
    на
    [assembly: AssemblyKeyFile("examplekey.key")]
    . Какой бы метод ни использовался, конечный результат получается тот же самый. CLR будет устанавливать ключ в файл с помощью Crypto Service Provider (CSP). Работа CSP не представлена в данном приложении.

    Все сборки в глобальном кэше сборок должны иметь устойчивое имя. Глобальный кэш сборок применяется для хранения сборок, специально созданных для общего использования несколькими приложениями на машине. Существует несколько способов размещения сборки в глобальном кэше сборок. Можно использовать программу установки, созданную для работы с глобальным кэшем сборок, .NET Framework SDK предоставляет программу установки с именем

    gacutil.exe
    . Чтобы поместить
    FirstLibrary.dll
    в глобальный кэш сборок, воспользуйтесь командой:

    gacutil -i FirstLibrary.dll

    Можно использовать Проводник Windows для перетаскивания сборок в кэш. Отметим, что необходимо иметь привилегии администратора на машине, чтобы устанавливать сборки в глобальный кэш сборок, независимо от используемого подхода.

    Типы данных

    Типы данных в Java и в C# можно сгруппировать в две основные категории: типы данных значений и ссылочные типы. Существует только одна категория типа данных значений в Java. Все типы данных значений являются по умолчанию примитивными типами данных языка. C# предлагает более обильный ассортимент. Типы данных значений можно разбить на три основные категории:

    □ Простые типы

    □ Типы перечислений

    □ Структуры

    Давайте рассмотрим каждый из них по очереди.

    Простые типы

    Ранее в разделе о ключевых словах было сделано подробное сравнение между примитивными типами данных Java и их эквивалентами в C# (по размеру). Был также введен ряд типов данных значений, представленных в C#, которых Java не имеет. Это были 8-битовый без знака

    byte
    (отличный
    от
    byte в Java, который имеет знак и отображается в
    sbyte
    в C#), короткое целое без знака
    ushort
    , целое без знака
    uint
    , длинное целое без знака
    ulong
    и, наконец, высокоточное
    decimal
    .

    Целые значения

    Когда целое число не имеет суффикса, то тип, с которым может быть связано его значение, оценивается в порядке

    int
    ,
    uint
    ,
    long
    ,
    ulong
    ,
    decimal
    . Целые значения представляются как десятичные или шестнадцатеричные литералы. В коде ниже результат равен 52 для обоих значений:

    int dec = 52;

    int hex = 0x34;

    Console.WriteLine("decimal {0}, hexadecimal {1}", dec, hex);

    Символьные значения

    char
    представляет одиночный символ Unicode длиной два байта. C# расширяет гибкость присваивания символов, допуская присваивание с помощью шестнадцатеричной кодированной последовательности с префиксом
    и представление Unicode с помощью
    \u
    . Также нельзя неявно преобразовать символы в целые числа. Все другие обычные кодированные последовательности языка Java полностью поддерживаются.

    Логические значения

    bool
    ,
    boolean
    в Java, используются для представления значений
    true
    и
    false
    непосредственно или как результат равенства, как показано ниже:

    bool first_time = true;

    cool second_time = (counter < 0);

    Значения decimal

    C# вводит тип данных

    decimal
    , который является 128-битовым типом данных, представляющим значения в диапазоне от примерно 1.0×1028 до 7.9×1028. Они предназначены прежде всего для финансовых и денежных вычислений, где точность является предельно важной. При присваивании типу
    decimal
    значения, к литеральному значению должно добавляться
    m
    , иначе компилятор считает значение типом
    double
    . Так как
    decimal
    не может неявно преобразовываться в
    double
    , то отсутствие m требует явного преобразования  типа:

    decimal precise = 1.234m;

    decimal precise = (decimal)1.234;

    Значения с плавающей точкой

    Значения с плавающей точкой могут быть либо

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

    float f = 5.6;

    Console.WriteLine(x);

    Этот пример будет создавать сообщение об ошибке компиляции, показанное ниже.

    C:\_wrox\c# for java developers\code\SuperEX\Class1.cs(15): Literal of type double cannot De implicitly converted to type 'float'; use an 'F' suffix to create a literal of this type

    Существует два способа решения этой проблемы. Можно преобразовать литерал во

    float
    , но сам компилятор предлагает более разумную альтернативу. Использование суффикса
    F
    говорит компилятору, что это литерал типа
    float
    , а не
    double
    . Хотя и не обязательно, но можно использовать суффикс
    D
    , чтобы указать на литерал типа
    Double
    .

    Типы перечислений

    Перечисление является отдельным типом, состоящим из множества именованных констант. В Java можно добиться этого, используя переменные

    static final
    . В этом смысле перечислении могут в действительности быть частью класса, который их использует. Другой подход состоит в определении перечисления как интерфейса. Пример ниже иллюстрирует такую концепцию:

    interface Color {

     static int RED = 0;

     static int GREEN = 1;

     static int BLUE = 2;

    }

    Этот подход проблематичен тем, что он не является безопасным в отношении типов данных. Любое считанное или вычисленное целое используется в качестве цвета. Можно, однако, программным путем реализовать перечисление с безопасными типами в Java, используя вариант шаблона (паттерна) Singleton, который ограничивает класс предопределенным числом экземпляров. Приведенный далее код показывает, как это можно сделать:

    final class Day {

     // final, поэтому нельзя создавать подклассы этого класса

     private String internal;

     private Day(String Day) {internal = Day;} // закрытый конструктор

     public static final Day MONDAY = new Day("MONDAY");

     public static final Day TUESDAY = new Day("TUESDAY");

     public static final Day WEDNESDAY = new Day("WEDNESDAY");

     public static final Day THURSDAY = new Day("THURSDAY");

     public static final Day FRIDAY = new Day("FRIDAY");

    }

    Как можно видеть из приведенного выше примера, перечисленные константы связаны не с примитивными типами, а с объектными ссылками. Также, поскольку класс определен как

    final
    , то нельзя создать его подклассы, следовательно, никакие другие классы не могут из него создаваться. Конструктор отмечен как закрытый, поэтому другие методы не могут использовать класс для образования новых объектов. Единственные объекты, которые будут созданы для этого класса, это статические объекты, которые класс формирует для себя при первой ссылке на класс.

    Можно согласиться с тем, что хотя концепция достаточно простая, обход включает развитую технику и она может не сразу стать понятной новичку, в конце концов нам требуется только список констант; C#, в противоположность, предоставляет встроенную поддержку перечислении, которая обеспечивает также безопасность типов. Чтобы объявить в C# перечисление, используется ключевое слово

    enum
    . В своей простейшей форме
    enum
    может выглядеть как следующий код:

    public enum Status {

     Working,

     Complete,

     BeforeBegin

    }

    В приведенном выше случае первое значение равно 0 и

    enum
    увеличивает значения.
    Complete
    будет 1 и т.д. Если по какой-то причине требуется, чтобы
    enum
    представлял другие значения, можно сделать это, присваивая такие значения следующим образом:

    public enum Status {

     Working = 131,

     Complete = 129,

     BeforeBegin = 132

    }

    Имеется также возможность использовать другие числовые целые типы 'наследуя' от

    long
    ,
    short
    или
    byte
    .
    int
    всегда является типом по умолчанию. Эта концепция проиллюстрирована ниже:

    public enum Status : int {

     Working,

     Complete,

     BeforeBegin

    }

    public enum SmallStatus : byte {

     Working,

     Complete,

     BeforeBegin

    }

    public enum BigStatus : long {

     Working,

     Complete,

     BeforeBegin

    }

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

    byte
    в C#, например, может содержать 1 байт памяти. Это означает, что
    SmallStatus
    не может с одержать более 255 констант или задать значение любой своей константы больше 255. Следующий листинг показывает, как можно использовать оператор
    sizeof()
    для идентификации различий между версиями
    Status
    :

    int х = sizeof(Status);

    int у = sizeof(SmallStatus);

    int Z = sizeof(BigStatus);

    Console.WriteLine("Regular size:\t{0}\nSmall size \t{1}\nLarge size:\t{2}", x, y, z);

    После компиляции листинг создаст результаты, показанные ниже:

    Regular size: 4

    Small size:   1

    Large size:   8

    Структуры

    Одним из основных различий между структурой C# (идентифицируемой ключевым словом

    struct
    ) и классом является то, что то умолчанию
    struct
    передается посредством значения, в то время как объект передается по ссылке. Как хорошо известно, объекты создаются в куче, в то время как переменные, которые на них ссылаются, хранятся в стеке. Структуры, со своей стороны, создаются и хранятся в стеке. Их аналога в Java не существует. Структуры имеют конструкторы и методы, у них могут быть индексаторы, свойства, операторы и даже вложенные типы. С помощью
    struсt
    создаются типы данных, которые ведут себя таким же образом, как встроенные типы. Ниже приведен пример использования структур:

    public struct WroxInt {

     int internalVal;

     private WroxInt(int x) {

      internalVal = x;

     }

     public override string ToString() {

      return Int.ToString(internalVal);

     }

     public static impicit operator WroxInt(int x) {

      return new WroxInt(x);

     }

    }

    public static void UseWroxInt() {

     WroxInt wi = 90;

     Console.WriteLine(wi);

    }

    Этот пример показывает типы, которыми владеют мощные структуры.

    WroxInt
    используется почти так же, как и встроенный тип
    int
    . Как известно, не существует способа сделать что-нибудь подобное в Java. Ряд других достоинств и ограничений, связанных с использованием структур, представлен ниже:

    □ 

    struct
    нельзя наследовать от другой
    struct
    или от класса.

    □ 

    struct
    не является базой для класса

    □ Хотя

    struct
    может oбъявлять конструкторы, эти конструкторы должны получать не меньше одного аргумента.

    □ Члены

    struct
    не могут иметь инициализаторов.

    □ Возможно создание экземпляра

    struct
    без использования ключевого слова
    new
    .

    □ 

    struct
    может реализовывать интерфейсы.

    Атрибуты используются со структурами чтобы добавить им дополнительную мощь и гибкость. Атрибут

    StructLayout
    в пространстве имен
    System.Runtime.InteropServices
    , например, применяется для определения компоновки полей в
    struct
    . Это свойство подходит и для создания структуры, аналогичной по функциональности
    union
    в С/C++,
    union
    является типом данных, члены которого находятся в одном блоке памяти. Он может использоваться для хранения значений различных типов в одном блоке памяти.
    union
    годится и в том случае, когда неизвестно, каким будет тип полученных значений. Конечно, никакого рeaльного преобразования не происходит, фактически не существует никакие базовых проверок допустимости данных. Один и тот же набор битов интерпретируется различным образом. Рассмотрим пример того, как
    union
    создается с помощью
    struct
    :

    [StructLayout(LayoutKind.Explicit)]

    public struct Variant {

     [FieldOffset(0)] public int intVal;

     [FieldOffset(0)] public string stringVal;

     [FieldOffset(0)] public decimal decVal;

     [FieldOffset(0)] public float floatVal;

     [FieldOffset(0)] public char charVal;

    }

    Атрибут

    FieldOffset
    , применяемый к полям, используется для задания физического расположения указанного поля. Задание начальной точки каждого поля как 0 гарантирует, что любое сохранение данных в одном поле перезапишет практически любые данные, которые там находятся. Отсюда следует, что общий размер полей равен размеру наибольшего поля, в данном случае
    decimal
    .

    Ссылочные типы

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

    extends
    и
    implements
    . Оператор двоеточия (
    :
    ) заменяет оба ключевых слова Java, и, как было показано ранее, директива
    using
    аналогична инструкции Java
    import
    . Строки тоже используются одинаково в C# и Java. C# вводит также новый тип ссылочного типа называемого делегатом. Делегаты представляют безопасную, с точки зрения типов, версию указателей функций. Они будут рассмотрены позже в этой главе.

    Массивы

    C# поддерживает "неровные" массивы и добавляет многомерные массивы. Может сбить с толку то, что Java не делает между ними различий:

    int [] х = new int[20]; // как в Java, только [] должны следовать

                            // за типом

    int [,] у = new int[12, 3]; // то же самое, что int у[] [] = new

                                // int[12][3];

    int[][] z = new int[5][]; // то же самое, что и int x[][] = new

                              // int [5][];

    Примечание. Ключевое слово

    int[]
    обозначает реальный тип данных, поэтому синтаксически оно записывается таким образом. Нельзя, как в Java, поместить двойные скобки перед или после переменной. Прежде чем перейти к дополнительным деталям о ссылочных типах и обсуждению таких концепции, как классы, давайте поговорим немного об операциях. Следующий раздел посвящен операторам.

    Операторы

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

    Присваивание

    C# и Java используют знак

    =
    для присваивания значений переменным. В C#, как и в Java, переменные присвоенные объектам содержат только ссылку или "адрес" на этот объект, а не сам объект. Присваивание одной ссылочной переменной другой таким образом просто копирует "адрес" в новую переменную. Следовательно обе переменные теперь имеют возможность делать ссылку на один объект. Эту концепцию легко проиллюстрировать с помощью примера. Рассмотрим класс
    ExOperators
    , приведенный ниже:

    public class EXOperators {

     internal int р;

     public EXOperators() {}

     public static void Main() {

      ExOperators one = new EXOperators();

      one.p = 200;

      EXOperators two;

      two = one;

      two.p = 100;

      Console.WriteLine(two.p);

      Console.WriteLine(one.p);

     }

    }

    Пока проигнорируем ключевое слово

    internal
    перед переменной
    р
    . Оно является модификатором доступа, который будет рассмотрен далее в этой главе. Достаточно сказать, что оно делает переменную
    р
    видимой для метода
    Main
    . Приведенный выше пример создает экземпляр объекта
    EXOperators
    и сохраняет его в локальной переменной
    one
    . Эта переменная затем присваивается другой переменной —
    two
    . После этого значение
    p
    в объекте, на который ссылается
    two
    , изменяется на 100. В конце выводится значение переменной
    p
    в обоих объектах. Компиляция и выполнение этого даст результат 100 дважды, указывая, что изменение
    two.р
    было тем же самым, что изменение значения
    one.р
    .

    Сравнение

    Операторы сравнения обычно совпадают по форме и функциональности в обоих языках. Четырьмя основными операторами являются

    <
    — меньше, чем,
    >
    — больше, чем,
    <=
    — меньше или равно и
    >=
    — больше или равно.

    Чтобы определить, принадлежит ли объект заданному классу или любому из классов предков, Java использует оператор

    instanceof
    . Простой пример этого приведен в листинге ниже:

    String у = "a string";

    Object х = у;

    if (х instanceof String) {

     System.out.println("х is a string");

    }

    В C# эквивалентом

    instanceof
    является оператор
    is
    . Он возвращает
    true
    , если тип времени выполнения заданного класса совместим с указанным типом. Версия C# приведенного выше кода будет иметь следующую форму:

    string у = "a string";

    object х = у;

    if (х is System.String) {

     System.Console.WriteLine("x is a string");

    }

    Операторы равенства aрифметические, условные, побитовые, битового дополнения и сдвига

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

    Преобразование типов

    Преобразование в Java состоит из неявного или явного сужения или расширения преобразования типа при использовании оператора

    ()
    . Можно выполнить аналогичное преобразование типа в C#. C# также вводит ряд действенных способов, встроенных в язык, среди них мы выделим упаковку и распаковку.

    Так как типы значений являются блоками памяти определенного размера, они прекрасно подходят для целей ускорения. Иногда, однако удобство объектов желательно иметь для типов значений. Упаковка и распаковка предоставляет механизм, который формирует линию соединения между типами значений и ссылочными типами, позволяя преобразовать их в и из объектного типа.

    Упаковка объекта означает неявное преобразование любого типа значения в объектный тип. Экземпляр объекта создается и выделяется, а значение из типа значения копируется в новый объект. Здесь приведен пример, показывающий, как упаковка работает в C#:

    // BoxEx.cs

    public class OverflowEX {

     public static void Main(String[] args) {

      int x = 10;

      Object obj = (Object)x;

      Console.WriteLine(obj);

     }

    }

    Такой тип функциональности недоступен в Java. Код, представленный ниже не будет компилироваться, так как примитивы не могут преобразовываться в ссылочные типы:

    // BoxEx.java

    public class BoxEX {

     public static void main(String args[]) {

      int x = 10;

      object obj = (object)x;

      System.out.println(obj);

     }

    }

    Распаковка является просто преобразованием объектного типа, приводящим значение снова к соответствующему типу значения. Эта функциональность опять же недоступна в Java. Можно изменить предыдущий код для иллюстрации этой концепции. Сразу заметим, что в то время как упаковка является неявным преобразованием типа, распаковка требует явного преобразования типа. Вот новая реализация

    BoxEx.cs
    :

    // BoxEX.cs

    public class OverflowEX {

     public static void Main(String[] args) {

      int x = 10;

      Object, obj = (Object)x;

      Console.WriteLine(obj);

      int у = (int)obj;

      Console.WriteLine(y);

     }

    }

    Другим эффективным способом C#, предназначенным для преобразования типов, является возможность определить специальные операторы преобразования. Определенные пользователем преобразования выполняются из типа данных в тип, а не из экземпляра в экземпляр, поэтому они должны быть статическими операциями. Можно использовать ключевое слово

    implicite
    для объявления определенных пользователем преобразований из одного типа в другой. Предположим, что имеются два класса
    Man
    и
    Car
    , которые полностью не связаны. Создадим определенное пользователем преобразование, которое переводит один класс в другой. Ниже приведен листинг
    Man.cs
    :

    public class Man {

     int arms, legs;

     string name;

     public Man(){}

     public int Arms {

      set {

       arms = value;

      }

      get {

       return arms;

      }

     }

     public string Name {

      set {

       name = value;

      }

      get {

       return name;

      }

     }

     public int Legs {

      set {

       legs = value;

      }

      get {

       return legs;

      }

     }

    }

    Как можно видеть из приведенного примера, класс

    Man
    имеет три свойства: можно задать или извлечь
    Legs
    ,
    Arms
    ,
    Name
    . Ниже представлен листинг класса
    Car
    :

    public class Car {

     int wheels, doors, headlights;

     public Car(int wheels, int doors, int headlights) {

      this.wheels = wheels;

      this.doors = doors;

      this.headlights = headlights;

     }

    }

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

    Car.wheel
    будет сопоставлено с
    Man.legs
    , а поле
    Car.doors
    с
    Man.arms
    . Не существует поля в
    Car
    , которое представляет что-нибудь похожее на
    Man.Name
    , но это не мешает использовать его. Можно, скажем, сопоставить
    Car.headlights
    с длиной строки, которая хранится в
    Man.name
    . Любая реализация, которая имеет смысл для программиста, будет приемлема. В этом случае
    Man.name
    не сопоставляется с
    Car.headlights
    , вместо этого для
    headlights
    жестко кодируется 2, когда делается преобразование, и полностью отбрасывается
    Man.name
    . Следующий код содержит модификацию класса
    Car
    :

    public class Car {

     int wheels, doors, headlights;

     public Car(int wheels, int doors, int headlights) {

      this.wheels = wheels;

      this.doors = doors;

      this.headlight = headlights;

     }

     public static implicit operator Car(Man man) {

      return new Car(man.Legs, man.Arms, 2);

     }

     public static explicit operator(Car car) {

      Man man = new Man();

      man.Arms = car.doors;

      man.Legs = car.wheels;

      man.Name = "john";

      return man;

     }

    }

    Мы добавим также переопределенные версии для методов

    ToString()
    обоих классов, чтобы вывести содержимое объекта
    Car
    . Это делается так:

    // для Man.cs

    public override string ToString() {

     return "[arms:" + arms + "|legs:" + legs + "|name:" + name + "]";

    }

    // для Car.cs

    public override string ToString() {

     return "[wheels:" + wheels + "|doors:" + doors + "|headlights:" + headlights + "]";

    }

    Листинг кода ниже показывает использование специального преобразования:

    // BoxEx.cs

    public class OverflowEX {

     public static void Main(String[] args) {

      Car car = new Car (4, 5, 2);

      Man man = (Man) car; // использует явное специальное преобразование

      Console.WriteLine("Man - ");

      Console.WriteLine(man);

      Console.WriteLine();

      Car car2 = man; // использует неявное специальное преобразование

      Console.WriteLine("Car - ");

      Console.WriteLine(car2);

     }

    }

    Компиляция и выполнение этого кода создает показанные ниже результаты:

    Man -

    [arms:5|legs:4|name:john]

    Car -

    [wheels:4|doors:5|headlights:2]

    Перезагрузка

    В начале важно отметить, что перезагрузка операторов не определена в CLS. Однако CLS обращается к ней, потому что языки, обеспечивающие ее функциональность, делают это способом, который могут понять другие языки. Таким образом, языки, которые не поддерживают перезагрузку операторов, все-таки имеют доступ к базовой функциональности. Java является примером языка, который не поддерживает перезагрузку операторов, — ни одна из концепций, рассмотренных в этом разделе, не может ее использовать. Спецификация среды .NET включает ряд рекомендаций для проведения перезагрузки операторов.

    □ Определите операторы на типах данных значений, которые логически являются встроенным типом языка (таким, как

    System.Decimal
    ).

    □ Используйте методы перезагрузки операторов, включающие только тот класс, на котором определены методы.

    □ Применяйте соглашения об именах и сигнатурах, описанные в CLS.

    □ Перезагрузка операторов полезна в случаях, где точно известно, каким будет результат операции.

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

    Перезагрузка операторов связана как с определенными пользователем преобразованиями, так и с типом данных, а не с экземпляром. Это означает, что она связана со всем типом данных, а не с каким-то одним экземпляром объекта, то есть операция всегда должна быть

    static
    и
    public
    .

    В нижеследующем примере создается тип данных значения

    Wheels
    , который может выполнять перезагруженное сложение с самим собой. Можно отметить частое использование комментариев и тегов типа XML внутри комментариев, они нужны для документации. Документация C# будет обсуждаться ниже в этом приложении:

    public struct Wheels {

     int wheel;


     // загрузить начальное значение в wheel

     private Wheels(int initVal); {

      wheel = initVal;

     }


     /// <summary>

     /// показывает внутреннее число wheels

     /// </summary>

     internal int Number {

      set {

       wheel = value;

      }

      get {

       return wheel;

      }

     }


     /// <summary>

     /// возвращает внутреннее число. Если этот метод

     /// не переопределен, то возвращаемой строкой будет тип Two.Wheels.

     /// </ summary >

     /// <returns></returns>

     public override string ToString() {

      return wheel.ToString();

     }


     /// < summary>

     /// выполнить операцию сложения на двух wheels

     /// </summary>

     /// <param name="w1"></param>

     /// <param name="w2"></param>

     /// <returns></returns>

     public static Wheels operator + (Wheels w1, Wheels w2) {

      w1.wheel += w2.wheel; return w1;

     }


     /// <summary>

     /// предоставляет альтернативную функциональность сложения.

     /// отметим, что вторая альтернатива операции сложения

     /// находится не в этой структуре, а в классе car

     /// </summary>

     /// <param name= "w"></param>

     /// <returns></returns>

     public Wheels AddWeels(Wheels w) {

      this.wheel += w.wheel;

      return this;

     }


     /// <summary>

     /// поэтому целые литералы можно неявно преобразовать в wheel

     /// </summary>

     /// <param name="x"></param>

     /// <returns></returns>

     public static implicit operator Wheels(int x) {

      return new Wheels(x);

     }

    }

    Здесь выделим использование метода

    AddWheel()
    , что удовлетворяет рекомендациям об альтернативных сигнатурах. Язык CLS, который не поддерживает перезагрузку операторов, может получить доступ к той же функциональности сложения с помощью этого метода. Фрагмент кода ниже показывает, как может использоваться этот тип данных значения:

    public static void Main(String[] args) {

     Wheels front = 2; // неявное преобразование

     Wheels back = 4; // неявное преобразование

     Wheels total = front + back; // перезагруженная версия сложения

     Console.WriteLine(total);

    }

    Компиляция и выполнение этого кода дадут в результате 6. Можно также изменить тип

    Car
    , чтобы разрешить сложение и вычитание из него
    Wheels
    . Следующий код показывает изменения, сделанные в классе
    Car
    :

    public class Car {

     int wheels, doors, headlights;


     public Car(int wheels, int doors, int headlights) {

      this.wheels = wheels;

      this.doors = doors;

      this.headlights = headlights;

     }


     public Car AddWheel(Two.Wheels w) {

      this.wheels += w.Number;

      return this;

     }


     internal int Wheels {

      set {

       wheels = value;

      }

      get {

       return wheels;

      }

     }


     /// <summary>

     /// выполняет операцию сложения на Wheel и Car

     /// </summary>

     /// <param name="c1">car</param>

     /// <param name="w1">wheel</param>

     /// <returns></returns>

     public static Car operator +(Car c1, Wheels w1) {

      c1.Wheels += w1.Number;

      return c1;

     }


     /// <summary>

     /// выполняет операцию вычитания на Wheel и Car

     /// </summary>

     /// <param name="c1">car</param>

     /// <param name="w1">wheel</param>

     /// <returns></returns>

     public static Car operator -(Car c1, Wheels w1) {

      c1.Wheels -= w1.Number;

      return c1;

     }


     public override string ToString() {

      return

       "[wheels = " + wheels + "| doors = " + doors + "|"

       + " headlights = " + headlights + "]";

     }

    }

    В класс

    Car
    также был добавлен метод
    AddWheel
    . Представленный далее фрагмент кода проверяет функциональность, только что добавленную в
    Car
    :

    public static void Main(String[] args) {

     Wheels front = 2;

     Wheels back = 4;

     Wheels total = front + back;

     Car greenFordExpedition = new Car(0, 4, 2);

     Console.WriteLine("initial:\t" + greenFordExpedition);

     greenFordExpedition += total;

     Console.WriteLine("after add:\t" + greenFordExpedition);

     greenFordExpedition -= front;

     Console.WriteLine("after subtract:\t" + greenFordExpedition);

    }

    Компиляция и выполнение этого кода создадут приведенные ниже результаты:

    initial:        CAR-[wheels = 0| doors = 4| headlights = 2 ]

    after add:      CAR-[wheels = 6| doors = 4| headlights = 2 ]

    after subtract: CAR-[wheels = 4| doors = 4| headlights = 2 ]

    sizeof и typeof

    Так как Java не имеет других типов данных значений, кроме примитивных, размер которых всегда известен, то реального применения для оператора

    sizeof
    нет. В C# типы данных значений охватывают примитивные типы, а также структуры и перечисления. Конечно, как и в Java, размер примитивных типов известен. Однако необходимо знать, сколько пространства занимает тип
    struct
    или
    enum
    , для чего служит оператор
    sizeof
    . Синтаксис достаточно простой:
    sizeof(<Value Type>)
    , где
    <Value Type>
    будет
    struct
    или
    enum
    . Необходимо отметить один момент при использовании оператора
    sizeof
    — он может использоваться только в ненадежном контексте. Оператор
    sizeof
    не может быть перезагружен.

    Оператор

    typeof
    используется для получения объекта типа экземпляра типа без создания экземпляра типа. В Java каждый тип имеет переменную класса
    public static
    , которая возвращает дескриптор объекта
    Class
    , ассоциированный с этим классом. Оператор
    typeof
    предоставляет функциональность этого типа. Так же как в случае
    sizeof
    , синтаксис будет очень простым:
    typeof(<Type>)
    , где
    <Type>
    является любым типом, определенным пользователем, который вернет объект типа этого типа.

    Делегаты

    Делегаты являются членами пространства имен, которые инкапсулируют ссылку на метод внутри объекта делегата. Объект делегата может затем передаваться в код, вызывающий указанный метод, не зная во время компиляции, какой метод будет вызван. Красоту, мощь и гибкость делегатов можно увидеть только с помощью примера. Давайте посмотрим, как работают делегаты:

    namespace Samples {

     using System;

     using System.Collections;

     public delegate void TestDelegate(string k); // определяет делегата,

      // который получает строку в качестве аргумента


     public class Sample {

      public Sample() {}

      public void test(string i) {

       Console.WriteLine(i + " has been invoked.");

      }

      public void text2(string j) {

       Console.WriteLine("this is another way to invoke {0}" + j);

      }

      public static void Main(string[] args) {

       Sample sm = new Sample();

       TestDelegate aDelegate = new TestDelegate(sm.test);

       TestDelegate anotherDelegate = new TestDelegate(sm.test2);

       aDelegate("test");

       anotherDelegate("test2");

      }

     }

    }

    Первый шаг по использованию делегатов состоит в определении делегата. Наш тестовый делегат определяется в строке

    public delegate void TestDelegate(string k);
     . Затем определяется класс с методами, которые имеют сигнатуру, аналогичную делегату. Конечный шаг заключается в создании экземпляра делегата который создается так же, как экземпляр класса и реализуется с помощью оператора
    new
    . Единственное различие состоит в том, что имя целевого метода передается делегату как аргумент. Затем вызывается делегат. В примере вызывается экземпляр
    aDelegate
    с помощью вызова
    aDelegate("test");
    .

    Подробно о классах

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

    Модификаторы

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

    Модификатор класса Описание
    abstract
    Нельзя создавать экземпляры абстрактных классов. Производные классы, которые их расширяют, должны реализовать все абстрактные методы класса, и модификатор
    sealed
    нельзя применять к этим классам.
    sealed
    Используется для предотвращения случайного наследования, так как от класса, определенного как
    sealed
    , нельзя наследовать.
    Модификатор члена Цель Эквивалент в Java Описание
    virtual
    Методы, методы доступа недоступно Позволяет переопределять целевые члены классам-наследникам.
    static
    Все
    static
    Целевой член, помеченный как
    static
    , принадлежит классу, а не экземпляру этого класса. Поэтому не требуется создавать экземпляр класса, чтобы получить к нему доступ.
    event
    Поля, свойства недоступно Используемый для связывания клиентского кода с событиями класса, модификатор
    event
    позволяет определить делегата, который будет вызываться, когда в коде произойдет некоторое "событие". Отметим, что программист класса определяет, где и когда инициируется событие, а подписчик определяет, как его обработать.
    abstract
    Методы, методы доступа
    abstract
    Указывает, что целевой член является неявно виртуальным и не имеет кода реализации. Производный класс должен предоставить эту реализацию, при этом реализованный метод помечается как
    override
    .
    const
    Поля, локальные переменные
    final
    Указывает, что целевой член не может быть изменен. Java также имеет ключевое слово
    const
    , которое в данный момент является просто зарезервированным словом.
    readonly
    Поля недоступно Указывает, что целевому члену можно присвоить значение только при его объявлении или в конструкторе класса, содержащего этот член.
    extern
    Методы недоступно Указывает, что целевой член реализуется внешне. Этот модификатор обычно используется с атрибутом
    DllImport
    .
    override
    Методы недоступно Указывает, что целевой член предоставляет новую реализацию члена, унаследованного из базового класса.
    Модификатор доступа Цель Эквивалент в Java Описание По умолчанию
    public
    Все
    public
    Без ограничений. Члены
    enum
    и
    interface
    , а также пространства имен.
    private
    Все
    private
    Доступны только объявляющему классу. Члены
    class
    и
    struct
    .
    internal
    Все недоступно Доступны файлам в той же сборке.  
    protected
    Все недоступно Доступны для объявляющего класса и любых его подклассов. В C#
    protected
    более ограничен, чем в Java. Закрытый (
    protected
    ) доступ не позволит другим файлам в той же сборке иметь доступ к члену.
     
    protected internal
    Все
    protected
    Доступны для файлов сборки и подклассов объявляющего класса.  

    Конструкторы

    Первый метод, который будет вызван в классе в процессе создания экземпляра объекта,— это конструктор. Утверждение справедливо для Java, C++, C# и других языков. Фактически, даже если специально не писать свой собственный конструктор, будет создан конструктор по умолчанию. Но в C# обращение к объекту-предку или другому конструктору обрабатывается совершенно по-другому, чем в Java:

    public class Parent {

    }

    public class Sample: Parent {

     private string internalVal;

     private string newVal;

     public Sample():base() {}

     public Sample(String s) {

      internalVal = s;

     }

     public Sample(String s, String t) : this(s) {

      newVal = t;

     }

    }

    Из этого примера видно, что выполнение вызова конструктора предка или даже другого конструктора можно сделать, "расширяя" его с помощью символа "

    :
    ". В случае конструктора предка используется ключевое слово
    base
    для идентификации источника, исходящего из объекта предка, в то время как это используется для идентификации источника, исходящего из другого конструктора объекта. Применение подходящей сигнатуры к
    base
    вызовет соответствующий конструктор предка, так же как применение правильной сигнатуры вызовет правильный внутренний конструктор. Мы подчеркнем это, делая некоторые изменения в класс
    Sample
    :

    public class Parent {

     protected Parent(string a) {

      Console.WriteLine(a);

     }

     protected Parent() {

      Console.WriteLine("This is the base constructor");

     }

    }

    public class Sample: Parent {

     public Sample() {

     }

     public Sample(String s):base(s) {

     }

     public Sample(String s, String t): this(s) {

      Console.WriteLine(t);

     }

    }

    C# вводит концепцию деструкторов, позаимствованную из C++. Они работают аналогично завершителям (

    finalizer
    ) в Java, их синтаксис, однако, существенно отличается. Деструкторы используют логический знак отрицания (
    ~
    ) в качестве префикса для имени класса:

    ~Sample() {

    }

    Рекомендация в отношении кода деструктора: "сборщик мусора" в .NET не вызывается сразу же после того, как переменная покидает область действия. На самом деле имеется некоторый интервал времени или условия памяти, которые инициируют поток выполнения. Бывают случаи, когда деструктор запускается в условиях нехватки памяти, поэтому желательно делать его код как можно короче. Также неплохо вызывать

    close
    на объектах, использующих много ресурсов, прежде чем разрушить контроллеры, которые их применяют.

    Методы

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

    in
    (это способ передачи параметров по умолчанию), по ссылке, как параметр
    ref
    , или как параметр
    out
    . Следующий код:

    public static void Main(string[] args) {

     int a = 10;

     Console.WriteLine(a);

     Add(a);

     Console.WriteLine(a);

    }

    public static void Add(int a) {

     a++;

    }

    будет создавать результат, показанный ниже, как в C#, так и в Java:

    10

    10

    Мы передаем

    а
    по значению, поэтому это значение не связано со значением в
    Main
    . Следовательно, увеличение а в методе
    Add
    не влияет на
    а
    в Main. Используя возможность, позволяющую передавать простые типы данных как ссылки, приведенный выше код можно изменить следующим образом:

    public static void Main(string[] args) {

     int a = 10;

     Console.WriteLine(a);

     Add(ref a);

     Console.WriteLine(a);

    }

    public static void Add(ref int a) {

     a++;

    }

    и получить:

    10

    11

    Чтобы использовать ссылочный параметр, надо перед типом параметра использовать ключевое слово

    ref
    . В противоположность двум другим типам параметров параметры
    out
    не нуждаются в инициализации, перед тем как они передаются в качестве аргументов, они используются для передачи значений назад из метода. Следующий код создаст результат 100:

    public static void Main(string[] args) {

     int a;

     Add(out a);

     Console.WriteLine(a);

    }

    public static void Add(out int a) {

     a = 100;

    }

    Еще одним удачным способом в C# является сокрытие метода. Концепция сокрытия метода обсуждалась ранее в этом приложении. Она позволяет иметь такую же сигнатуру, как и у метода базового класса, не переопределяя базовый метод. Это делается с помощью ключевого слова new, которое помещается перед реализацией метода. Отметим, что, как описано ранее, отсутствие ключевого слова

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

    namespace Sample {

     using System;

     public class SuperHider {

      public string Test() {

       return "parent test";

      }

     }

     public class Hider: SuperHider {

      public Hider() {

      }

      new public string Test() {

       return "child test";

      }

     }

    }

    Следующий листинг показывает, как вызывается любая версия метода

    Test()
    :

    Rider hider = new Hider();

    Console.WriteLine(hider.Test());

    Console.WriteLine(((SuperHider)h).Test());

    Результатом этих вызовов будет:

    Child test

    Parent test

    Сокрытие методов существенно отличается от переопределения методов. В C# переопределение метода является явной процедурой. Это отличается от подхода Java, где переопределение является поведением по умолчанию, когда сигнатура члена суперкласса совпадает с сигнатурой в его подклассе. Чтобы переопределить метод базового класса в C#, необходимо пометить его как

    virtual
    . К счастью нельзя просто изменить класс
    Hider
    , что показано в данном примере:

    namespace Samples {

     using System; public class SuperHider {

      public string Test() {

       return "parent test";

      }

     }

     public class Hider: SuperHider {

      public Hider() {

      }

      public override string Test() {

       return "child test";

      }

     }

    }

    Этот код не будет компилироваться. Надо сначала проинформировать компилятор, что указанный метод, в данном случае

    SuperHider.test()
    , может быть переопределен классами потомками. Для этого в C# используется ключевое слово
    virtual
    , а методы, к которым применяется этот модификатор, называются виртуальными методами. Возьмем пример подходящего способа выполнения переопределения метода:

    namespace Samples {

     using System;

     public class SuperHider {

      public virtual string Test() {

       return "parent test";

      }

     }

     public class Hider: SuperHider {

      public Hider() { }

      public override string Test() {

       return "child test";

      }

     }

    }

    Достоинством переопределения метода является гарантия, что будет вызван самый производный метод. Взгляните на код вызова, представленный ниже, такой же код, что и в примере сокрытия метода, создает два других значения:

    Hider hider = new Hider();

    Console.WriteLine(hider.Test());

    Console.WriteLine(((SuperHider)hider).Test());

    Так как гарантировано, что всегда вызывается версия

    test
    из
    Hider
    , мы знаем, что компиляция и выполнение кода всегда дадут следующие результаты:

    Child test

    Child test

    Единственное синтаксическое различие между абстрактными классами в Java и C# состоит в размещении ключевого слова

    abstract
    . Как и в Java, определение абстрактных методов в C# делает класс абстрактным.

    Свойства и индексаторы

    Раньше методы

    get()
    и
    set()
    использовались для доступа к внутренним атрибутам объекта. Сегодня C# вводит концепцию аксессоров (
    accessor
    ), которые предоставляют безопасный и гибкий способ получения внутри лих полей, Существует два типа аксессоров. Аксессор get разрешает чтение внутренних полей объекта, а аксессор set позволяет изменять значение внутреннего поля. Ключевое слово
    value
    представляет новое значение справа от знака равенства во время присваивания. Отсутствие соответствующего аксессора в объявлении свойства приведет к тому, что свойство будет предназначаться либо только для чтения (нет
    set
    ), либо только для записи (нет
    get
    ):

    namespace Samples {

     using System;

     public class Properties {

      private int age;

      private string name;

      public Properties(string name) {

       this.name = name;

      }

      public int Age {

       get {

        return age;

       }

       set {

        age = value;

       }

      }

      public string Name {

       get {

        return name;

       }

      }

     }

    }

    В указанном примере свойство

    Age
    имеет аксессоры
    get
    и
    set
    , поэтому можно читать или записывать в это свойство. Свойство
    Name
    , однако, создается один раз, когда создается новый экземпляр объекта свойств, после чего можно только прочитать значение
    Name
    . Свойства доступны, как если бы они были открытыми полями:

    Properties props = new Properties("john");

    props.Age = 21;

    Console.WriteLine("My name is {0}, and I am {1} years old.", props.Name, props.Age);

    Результатом этого кода является:

    My name is john, and I am 21 years old.

    Примечание. Имена свойств должны быть уникальными.

    Как предполагает название, индексаторы позволяют легко индексировать атрибуты объектов. Предположим, что нам необходимо предоставить аналогичную функциональность, не создавая двух отдельных свойств. Можно проиндексировать имеющиеся поля, чтобы они были доступны с помощью некоторого ключа (ключ является значением, используемым для доступа к индексу, для массивов ключ является целым значением), или извлечь объявления двух свойств в примере выше и заменить их следующим:

    public string this[string a] {

     get {

      if (a.Equals("Age")) return int.ToString(age);

      else if (a.Equals("Name")) return name;

      else {

    throw new Exception("can only accept 'name' or 'age' key");

      }

     }

     set {

      if (a.Equals("Age")) age = int.Parse(value);

      else {

    throw new Exception(a + " is read only or does not exist");

      }

     }

    }

    Затем можно обратиться к атрибутам свойств следующим образом:

    Properties props = new Properties("john");

    props["Age"] = "21";

    Console.WriteLine("my name is {0}, I am {1} years old.", props["Name"], props["Age"]);

    В результате мы получим:

    My name is john, I am 21 years old.

    События

    События C# предоставляют значительно более надежный и гибкий паттерн наблюдателя, чем в Java. Более того, они могут быть объявлены либо как поля, либо как свойства. Создание события является процессом из трех частей. Сначала мы получаем делегата, а затем событие, связанное с этим делегатом, и наконец, когда происходит некоторое действие, вызывается событие.

    Проиллюстрируем это, удаляя исключения из класса

    Properties
    и используя вместо этого события. Наиболее привлекательным свойством событий является гибкость, которая наблюдается при использовании модели делегата. По сути можно использовать любую специальную сигнатуру, которую желательно связать с событием, а затем подписчик на событие должен предоставить целевой метод, который соответствует требуемому списку параметров. Создание этой специальной сигнатуры начинается с определения делегата в коде пространства имен. Для класса
    Properties
    необходимо, чтобы он инициировал строковые события, аналогичные исключениям, которые необходимо порождать:

    public delegate void ObservableDelegate(string message);

    Затем объявляется

    delegate
    как поле
    event
    в классе:

    public event ObservableDelegate ExceptionEventListener;

    Наконец, переписывается реализация индексатора для активизации приемника события всякий раз, когда возникает условие исключения:

    public string this[string а] {

     get {

      if (a.Equals("Age")) {

       return int.ToString(age);

      } else if (a.Equals("Name")) {

       return name;

      } else {

    ExceptionEventListener(can only accept 'name' or 'age' key");

       return null; // поток программы продолжается после того, как

                    // событие было инициировано, поэтому необходимо

                    // вернуть значение. В этом случае, так как ключ

                    // является недействительным (не 'name' или 'age'),

                    // возвращается null, указывая отсутствие значения

      }

     }

     set {

      if (a.Equals("Age") {

       age = int.Parse(value);

      }

      else {

       listener(a+ " is read only or does not exist");

      }

     }

    }

    Экземпляр делегата, связанный с приемником событий, никогда не создается. Это объясняется тем, что создание экземпляра реально происходит на клиенте, который потребляет это событие. Для клиента событие представляется как открытое поле, но не надо думать, что оно не имеет ограничений. Единственными возможными действиями по отношению к полю события являются:

    □ Создание в событии новых экземпляров делегатов

    □ Удаление экземпляров делегатов из события

    C# использует операторы

    +=
    и
    -=
    соответственно для добавления и удаления экземпляров делегатов из событий. Оба оператора одинаковы в C# и Java. Недопустимо задавать событие, равным одному любому экземпляру делегата. Вместо этого можно добавить в событие столько делегатов, сколько понадобится. Это свободно транслируется в требуемое количество приемников событий для одного события. Пример ниже показывает, как это можно сделать:

    public delegate void TestEvent();

    public class Tester {

     public event TestEvent testEvent;

     Tester() { }

     public void Perform() {

      testEvent();

     }

     public class Client {

      Client() {

       Tester tester = new Tester();

       TestEvent a = new TestEvent(Callback1); // сначала создать делегата

       tester.testEvent += a; // затем добавить его

       tester.testEvent += new Test(CallBack2); // или можно сделать это

                                                // за один проход

       tester.testEvent += new Test(Callback3);

       tester.testEvent += new Test(Callback4);

       tester.Perform();

      }

      public void CallBack1() {

       // уведомить через e-mail)

      }

      public void CallBack2() {

       // послать факсы

      }

      public void CallBack3() {

       // послать беспроводное сообщение

      }

      public void CallBack4() {

       // сохранить в журнале

      }

     }

    }

    Как можно понять из приведенного примера, чтобы использовать события класса, необходимо сначала получить метод в классе подписчиков для обработки события (очень похоже на то, как действуют делегаты), затем добавить методы обработки события в событие. Давайте создадим статический метод

    Notify()
    :

    public static void Notify(string i) {

     Console.WriteLine(i);

    }

    Этот метод использует такую же сигнатуру, что и приемник событий класса

    Properties
    . В методе
    Main
    можно зарегистрировать метод
    Notify()
    и задать условно ошибку, чтобы протестировать событие:

    Properties props = new Properties("hello"); // зарегистрировать обработчик событий

    props.ExceptionEventListener += new ExceptionEventListener(test);

    p["Aged"] = "35"; // неправильный ключ используется

                      // для моделирования ошибки

    Исключения

    Исключения в C# на первый взгляд являются такими же, как в Java. Инструкции C#

    try-catch
    и
    try-catch-finally
    работают подобно своим аналогам в Java (смотрите раздел о ключевых словах). Однако в C# нельзя использовать инструкцию
    throws
    , поэтому невозможно указать вызывающей стороне, что некоторый код в методе может порождать исключение. Также имеется
    try-finally
    , который не подавляет порожденные исключения, но предлагает блок
    finally
    , выполняющий после порождения исключения, чтобы произвести очистку.

    Порождение исключений делается с помощью инструкции

    throw
    . Например, чтобы породить
    SystemException
    , используется код
    throw new SystemException (<arg-list>);
    . Это полностью совпадает c тем, как исключения порождается в Java. Требуется только инструкция
    throws
    и подходящий класс исключения. Далее представлен список некоторых стандартных классов исключений, предоставляемых средой выполнения .NET. Так же как в Java, их функциональность отражается в данных им именах:

    □ 

    Exception
    — базовый класс для всех объектов исключений.

    □ 

    SystemException
    — базовый класс для всех ошибок, создаваемых во время выполнения.

    □ 

    IndexOutOfRangeException
    возникает, когда индекс массива во время выполнения оказывается вне границ заданного диапазона.

    □ 

    NullReferenceException
    порождается, когда во время выполнения ссылаются на
    null
    .

    □ 

    InvalidOperationException
    порождается некоторыми методами, когда вызов метода бывает не действителен для текущего состояния объекта.

    □ 

    ArgumentException
    — базовый класс всех исключений для аргументов.

    □ 

    ArgumentNullException
    порождается если аргумент задан как
    null
    , когда это недопустимо.

    □ 

    InteropException
    является базовым классом для исключений, которые возникают или направлены на среды вне CLR.

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

    System.OverflowException
    , связанное с вычисленными результатами, превосходящими диапазон значений типа данных переменной результата. Инструкции
    checked
    и
    unchecked
    могут инициировать или подавлять связанные с этим исключения. Дополнительная информация о
    checked
    и
    unchecked
    находится в разделе данного приложения о ключевых словах.

    Условная компиляция

    Препроцессор в C# эмулируется. Он выполняется как отдельный процесс, прежде чем компилятор начнет свою работу. Поддерживаемые здесь директивы больше всего соответствуют C++, чем какому-либо другому языку. Конечно, в Java не существует эквивалентов функциональности, описанных в этом разделе. Разрешается определять символы, которые проверяются с помощью простых условных директив. Те, которые оказываются

    true
    , включаются и компилируются, иначе код игнорируется. Определение символа может происходить двумя способами. Прежде всего с использованием ключа компилятора
    /define
    , за которым следует двоеточие и определяемый символ, например:

    csc /define:TEST_TEST samples.cs

    Можно также определить символы на странице конфигурационных свойств проекта, добавляя их в разделенный двоеточиями список констант условной компиляции. Или пойти программным путем, используя директиву

    #define
    . В этом случае директива должна появиться раньше, чем что-либо другое, и применяется ко всем лексемам в области действия файла. Здесь перечислены допустимые условные директивы:

    □ 

    #if
    используется для проверки существования символа

    □ 

    #elif
    позволяет добавить несколько ветвей к инструкции
    #if

    □ 

    #else
    предоставляет окончательное альтернативное условие для
    #if
    и
    #elif

    □ 

    #endif
    заканчивает инструкцию
    #if

    namespace Samples {

     using System;

    #if EXAMPLE

     public class Example {

      public Example() {

      }

     }

    #elif TEST_TEST

     public class Test {

      public Test() {

      }

     }

    #else

     public class None {

      public None() {

      }

     }

    #endif

    }

    Добавление инструкции

    #define TEST_TEST
    делает класс
    Test
    видимым для компиляции, a
    #define EXAMPLE
    делает класс
    Example
    видимым для компиляции. Если ничего не добавлять, то будет компилироваться класс
    None
    . Для препроцессора C# доступны также две другие директивы —
    #warning
    и
    #error
    . Они должны помещаться в условное выражение
    #if
    . Попробуйте добавить следующую строку под инструкцией
    #else
    в приведенный код:

    #warning I wouldn't try to instantiate the example object if I were you

    C# также поддерживает условную функциональность. В коде ниже добавление условного атрибута в

    AMethod()
    делает его компилируемым только в том случае, когда определен символ
    Test
    :

    [conditional("TEST_TEST")]

    public void AMethod() {

     string s = "I am available only when Test is defined";

    }

    Вопросы безопасности

    В настоящее время код может приходить из разных источников. В Java, до появления Java 2, существовала установка, что все приложения, которым разрешается использовать все свойства языка, должны быть абсолютно надежными. Последующий опыт показал, что такой подход может быть достаточно опасным. Java теперь предоставляет службы политики безопасности с помощью файла

    java.policy
    . Приложения подвергаются той же проверке безопасности, что и апплеты. Политика безопасности может редактироваться напрямую или через
    policytool
    для создания приложений, в которых есть ограничения. Среда .NET для решения этой проблемы использует систему безопасности доступа к коду, которая контролирует доступ к защищенным ресурсам и операциям. Ниже представлен список наиболее важных функций системы безопасности доступа к коду:

    □ Код может требовать, чтобы вызывающая сторона имела специальные полномочия.

    □ Выполнение кода ограничено временем выполнения, в этом случае проводятся проверки, которые определяют, соответствуют ли предоставленные полномочия вызывающей стороны требуемым полномочиям операций.

    □ Код может запрашивать полномочия, которые ему требуются для выполнения и полномочия, которые будут полезны, а также явно утверждать, какие полномочия он никогда не должен иметь.

    □ Определяются полномочия, которые представляют определенные права для доступа к различным системным ресурсам.

    □ Администраторы могут выбирать политику системы безопасности, которая присваивает определенные полномочия определенным группам кода.

    □ Система безопасности доступа к коду предоставляет полномочия, когда компонент загружается. Это предоставление основывается на запросе кода, а также на операциях, разрешенных системой безопасности.

    Обеспечение политики безопасности делает надежной среду управляемого кода .NET. Это объясняется тем, что каждая загружаемая сборка подчиняется политике безопасности, которая предоставляет полномочия для кода на основе доверия, где доверие базируется на признаках данного кода. Система безопасности .NET позволяет коду использовать защищенные ресурсы, только если он имеет на это "полномочие". Код запрашивает полномочия, которые ему требуются, а политика безопасности, применяемая .NET, определяет, какие полномочия будут реально предоставлены коду. Среда .NET предоставляет в C# классы полномочий доступа к коду, каждый из которых инкапсулирует возможность доступа к определенному ресурсу. Связанный с каждым полномочием класс является перечислением флагов полномочий, используемых для определения конкретного флага полномочий доступа к объекту. Эти полномочия используются для указания .NET, что надо разрешить коду делать и что должно быть разрешено вызывающей этот код стороне. Политика использует эти объекты также для определения, какие полномочия дать коду. Далее следует список стандартных полномочий:

    □ 

    EnvironmentPermission
    : определяет полномочия доступа для переменных окружения. Существуют два возможных типа доступа — только для чтения и только для записи. Доступ для записи предоставляет также полномочия для создания и удаления переменных окружения.

    □ 

    FileIOPermission
    : существуют три возможных типа полномочий ввода/вывода для файлов — чтение, запись и добавление. Чтение и запись самоочевидны, добавление ограничивается только добавлением, что читать остальное не разрешается.

    □ 

    ReflectionPermission
    : управляет возможностью чтения информации о типе неоткрытых членов типа, а также использованием
    Reflection.Emit
    .

    □ 

    RegistryPermission
    : управляет чтением, записью и созданием в реестре.

    □ 

    SecurityPermission
    : управляет совокупностью флагов полномочий, используемых системой безопасности.

    □ 

    UIPermission
    : управляет доступом к различным аспектам интерфейса пользователя.

    □ 

    FileDialogPermission
    : управляет доступом к файлам на основе диалогового окна системного файла.

    □ 

    IsolatedStoragePermission
    : управляет доступом к изолированной памяти.

    В C# существуют два способа изменения текущих полномочий безопасности с помощью использования: вызовов для классов полномочий в средах .NET или атрибутов полномочий безопасности.

    Заключение

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

    C# использует среду .NET и поэтому предлагает встроенный, обеспечивающий безопасность типов, объектно-ориентированный код, взаимодействующий с любым языком, который поддерживает CTS (общую систему типов). Java может работать с С и C++, но без обеспечения безопасности типов. Более того, это достаточно сложно. В то же время C# предоставляет перезагрузку операторов, a Java этого не делает.

    Имена файлов в C# не связаны с классами в них, как это предусмотрено в Java, а также имена пространств имен не связаны с папками, как имена пакетов в Java. C# вводит концепцию делегатов — указателей функций, которые могут использоваться для инкапсуляции метода с определенной сигнатурой. Также C# предлагает множество встроенных типов данных значений, включая обеспечивающие безопасность типов перечисления, структуры и встроенные примитивы, которые представляют достойную альтернативу примитивам Java.

    В C# реализовано двунаправленное преобразование между ссылками и типами данных значений, называемое упаковкой и распаковкой. Эта функциональность не поддерживается в Java. C# поддерживает использование классов, укомплектованных полями, конструкторами и методами в качестве шаблонов для описания типов данных, и предоставляет возможность определить деструкторы — методы, вызываемые перед тем, как класс попадает к сборщику мусора. C# предоставляет также три подхода к параметрам методов —

    in
    ,
    out
    или
    ref
    , где по умолчанию используется
    in
    .

    C# вводит также концепцию сокрытия методов, а также поддержку явного переопределения с помощью ключевых слов

    virtual
    и
    override
    . В C# предусмотрены свойства как альтернатива методам
    getXXX()
    и
    setXXX()
    , обеспечивающие способ безопасного доступа к внутренним полям. Кроме того, C# допускает создание индексаторов для предоставления индексного доступа к внутренним полям объекта. В отличие от Java, однако, в C# нет возможности объявить, что метод может порождать исключение.

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







     

    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх