Книга: C# для профессионалов. Том II

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

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

Как известно, ключевое слово является специальным зарезервированным словом языка. Мы уже встречали некоторые из них допустим, объявление переменной как целого числа с помощью 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);
   }
  }
 }
}

Оглавление книги


Генерация: 0.202. Запросов К БД/Cache: 0 / 2
поделиться
Вверх Вниз