Симон Робинсон - C# для профессионалов. Том II
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 проверяет переполнение одиночных выражений: