Компонентный подход в программировании

       

Пользовательские типы


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

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

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

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

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

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

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

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

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

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

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

public interface Queue <T> { void put (T o); T get (); int size(); } using System;

public interface IQueue <T> { void Put (T o); T Get (); int Size(); } public class LinkedQueue <T> implements Queue <T> { public void put (T o) { if(last == null) { first = last = new Node <T> (o); } else { last.next = new Node <T> (o); last = last.next; } size++; }



public T get () { if(first == null) return null; else { T result = first.o; if(last == first) last = null; first = first.next; size--; return result; } }

public int size() { return size; }

private Node <T> last = null; private Node <T> first = null; private int size = 0;

private static class Node <E> {

E o = null; Node<E> next = null;

Node (E o) { this.o = o; } } } public class LinkedQueue <T> : IQueue <T> { public void Put (T o) { if(last == null) { first = last = new Node <T> (o); } else { last.next = new Node <T> (o); last = last.next; } size++; }

public T Get () { if(first == null) return default(T); else { T result = first.o; if(last == first) last = null; first = first.next; size--; return result; } }

public int Size() { return size; }



private Node <T> last = null; private Node <T> first = null; private int size = 0;

internal class Node <E> {

internal E o = default(E); internal Node <E> next = null;

internal Node <E> (E o) { this.o = o; } } } public class Program { public static void main(String[] args) { Queue<Integer> q = new LinkedQueue<Integer>();

for(int i = 0; i < 10; i++) q.put(i*i);

while(q.size() != 0) System.out.println ("Next element + 1: " + (q.get()+1)); } } public class Program { public static void Main() { Queue<int> q = new LinkedQueue<int>();

for(int i = 0; i < 10; i++) q.Put(i*i);

while(q.Size() != 0) Console.WriteLine ("Next element + 1: " + (q.Get()+1)); } }

Пример 10.1.

Обе программы выдают на консоль текст

Next element + 1: 1 Next element + 1: 2 Next element + 1: 5 Next element + 1: 10 Next element + 1: 17 Next element + 1: 26 Next element + 1: 37 Next element + 1: 50 Next element + 1: 65 Next element + 1: 82

На основе пользовательского или примитивного типа можно строить массивы элементов данного типа. Тип массива является ссылочным и определяется на основе типа элементов массива. Количество элементов массива в обоих языках — это свойство конкретного объекта-массива, которое задается при его построении и далее остается неизменным.


В обоих языках можно строить массивы массивов и пр.



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

int[] array = new int[3]; String[] array1 = new String[]{"First","Second"}; int[][] arrayOfArrays = new int[][] {{1, 2, 3}, {4, 5}, {6}};


В C# есть возможность строить многомерные массивы в дополнение к массивам массивов.

int[] array = new int[3]; string[] array1 = new string[]{"First","Second"}; int[][] arrayOfArrays = new int[][] {{1, 2, 3}, {4, 5}, {6}}; int[,] twoDimensionalArray = new int[,] {{1, 2}, {3, 4}};


Количество элементов в массиве доступно как значение поля length, имеющегося в каждом типе массивов.


Любой тип массива наследует системному типу System.Array, и любой объект-массив имеет все свойства и методы этого типа.

Общее количество элементов в массиве (во всех размерностях) доступно как значение, возвращаемое свойством Length. Количество измерений в массиве — значение свойства Rank.
В обоих языках есть возможность декларировать перечислимые типы (enums), объекты которых представляются именованными константами. Однако реализована эта возможность по-разному.



В Java перечислимые типы (введены в Java 5) являются ссылочными, частным случаем классов. По сути, набор констант перечислимого типа — это набор статически (т.е. во время компиляции, а не в динамике, во время работы программы) определенных объектов этого типа.

Невозможно построить новый объект перечислимого типа — декларированные константы ограничивают множество его возможных значений. Любой его объект совпадает с одним из объектов-констант, поэтому их можно сравнивать при помощи оператора ==.

Пример декларации перечислимого типа приведен ниже.


В C# перечислимые типы являются типами значений, определяемыми на основе некоторого целочисленного типа (называемого базовым; по умолчанию это int). Каждая константа представляет собой некоторое значение базового типа. Однако можно искусственно построить другие значения перечислимого типа из значений его базового типа.

Пример декларации перечислимого типа приведен ниже.


public enum Coin { PENNY ( 1), NICKEY ( 5), DIME (10), QUARTER(25);

Coin(int value) { this.value = value; }

public int value() { return value; }

private int value; }


public enum Coin : uint { PENNY = 1, NICKEY = 5, DIME = 10, QUARTER = 25 }


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

Возможны декларации методов перечислимого типа и их отдельная реализация для каждой из констант.

public enum Operation { ADD { public int eval(int a, int b) { return a + b; } }, SUBTRACT { public int eval(int a, int b) { return a - b; } }, MULTIPLY { public int eval(int a, int b) { return a * b; } }, DIVIDE {

public int eval(int a, int b) { return a / b; } };

public abstract int eval (int a, int b); }


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

Структуры описываются во многом похоже на классы, они так же могут реализовывать интерфейсы и наследовать классам, но имеют ряд отличий при использовании, инициализации переменных структурных типов и возможности описания различных членов. Все эти особенности связаны с тем, что структуры — типы значений. Две переменных структурного типа не могут иметь одно значение — только равные. Значение структурного типа содержит все значения полей. При инициализации полей структуры используются значения по умолчанию для их типов (0 для числовых, false для логического типа, null для ссылочных типов и построенное так же значение по умолчанию для структурных).

Пример декларации структуры приведен ниже.

public struct Point2D { private double x; private double y;

public Point2D(double x, double y) { this.x = x; this.y = y; }

public double X() { return x; } public double Y() { return y; } }

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

При переводе значений между структурным типом и object производятся их преобразования, называющиеся упаковкой (autoboxing) — построение объекта, хранящего значение структурного типа, — и распаковкой (auto-unboxing) — выделение значения из хранящего его объекта.

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

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


В Java, помимо явно описанных типов, можно использовать анонимные классы (anonymous classes).

Анонимный класс всегда реализует какой-то интерфейс или наследует некоторому классу. Когда объект анонимного класса создается в каком-то месте кода, все описание элементов соответствующего класса помещается в том же месте. Имени у анонимного класса нет.

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

interface Stack <T> { void push(T o); T pop (); }

public class B { public void m() { Stack<Integer> s = new Stack<Integer>() { final static int maxSize = 10; int[] values = new int[maxSize]; int last = -1;

public void push(Integer i) { if(last + 1 == maxSize) throw new TooManyElementsException(); else values[++last] = i; }

public Integer pop() { if(last – 1 < -1) throw new NoElementsException(); else return values[last--]; } };

s.push(3); s.push(4); System.out.println(s.pop() + 1); } }



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

Эта возможность еще отсутствует в версии стандарта [8], о ней можно прочитать в [9].

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

bool? mayBeNullFlag = null; int? mayBeNullNumber = 5; Point2D? mayBeNullPoint = null;

Обозначение T? является сокращением от System.Nullable<T>. Этот тип имеет свойство HasValue, возвращающее true тогда, когда его значение является значением типа T, а не null, и свойство Value, возвращающее это значение, если оно не null.

Определено неявное преобразование значений типа T в T?.

Для обнуляемых типов, построенных на основе примитивных, определены все те же операции.

Арифметические действия над значениями обнуляемых типов возвращают null, если один из операндов равен null.

Сравнения по порядку (<, >, <=, >=) возвращают false, если один из операндов равен null.

Можно использовать сравнение значений обнуляемых типов на равенство или неравенство null.

Для типа bool? операции & и | возвращают не null, если их результату можно приписать логическое значение. Т.е. false & null == false, а true | null == true. Также выполнены равенства, получаемые при перестановке операндов в указанных примерах.

Имеется специальный оператор ??, применимый к объектам ссылочных типов или к значениям обнуляемых типов.

Значение a??b равно (a != null)?a:b.

Кроме перечисленных разновидностей типов, в C# имеется возможность определять ссылочные типы, являющиеся аналогами указателей на функцию в C — делегатные типы (delegate types).

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

Объект делегатного типа можно инициализировать с помощью подходящего по типам параметров и результата метода или с помощью анонимного метода (anonymous method, введены в C# 2.0) [9].

В приведенном ниже примере объявляется делегатный тип BinaryOperation и 6 объектов этого типа, инициализируемых различными способами.

Объекты op1 и op3 инициализируются при помощи статического метода A.Op1(), объекты op2 и op4 — при помощи метода Op2(), выполняемого в объекте a, объекты op5 и op6 — при помощи анонимных методов.

public delegate int BinaryOperation (int x, int y); public class A { private int x = 0; public A(int x) { this.x = x; }

public static int Op1(int a, int b) { return a + b; }

public int Op2(int a, int b) { return x + a + b; }

public static A a = new A(15); BinaryOperation op1 = A.Op1; BinaryOperation op2 = a.Op2; BinaryOperation op3 = new BinaryOperation(A.Op1); BinaryOperation op4 = new BinaryOperation(a.Op2); BinaryOperation op5 = delegate(int c, int d) { return c * d; }; BinaryOperation op6 = delegate { return 10; }; }

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

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

Поэтому каждый объект-делегат представляет некоторый список операций (invocation list). При этом пустой список представляется как null.

Добавлять элементы в конец этого списка можно при помощи операторов + и +=, применяемых к делегатам (и методам, которые неявно преобразуются в объекты делегатного типа, как видно из инициализации op1 и op2 в примере). При объединении двух делегатов список операций результата получается конкатенацией списков их операций — список операций правого операнда пристраивается в конце списка операций левого операнда. Списки операций операндов не меняются.

Удалять операции из делегатов можно при помощи операторов - и -=. При вычитании одного делегата из другого находится последнее вхождение списка операций второго операнда как подсписка в список операций первого операнда. Список операций результата получается как результат удаления этого подсписка из списка операций первого операнда. Если этот список пуст, результат вычитания делегатов равен null. Если такого подсписка нет, список операций результата совпадает со списком операций первого операнда.

Объект делегатного типа можно вызвать, передав ему набор аргументов. При этом вызываются друг за другом все операции из представляемого им списка. Если объект-делегат равен null, в результате вызова выбрасывается исключение типа System.NullReferenceException.

using System;

public class A { delegate void D();

static void M1() { Console.WriteLine("M1 called"); } static void M2() { Console.WriteLine("M2 called"); }

public static void Main() { D d1 = M1, d2 = M2; d1 += M1; d2 = d1 + d2 + d1;

d1(); Console.WriteLine("------------"); d2(); Console.WriteLine("------------"); (d1 + d2)(); Console.WriteLine("------------"); (d1 - d2)(); Console.WriteLine("------------"); (d2 - d1)(); } }

Программа из приведенного примера выдает следующий результат.

M1 called M1 called ------------ M1 called M1 called M2 called M1 called M1 called ------------ M1 called M1 called M1 called M1 called M2 called M1 called M1 called ------------ M1 called M1 called ------------ M1 called M1 called M2 called

<


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