Rationale for Ada 2005: Object oriented model

RUSTOP
BACKNEXT

ENG

4. Interfaces

@ In Ada 95, a derived type can really only have one immediate ancestor. This means that true multiple inheritance is not possible although curious techniques involving discriminants and generics can be used in some circumstances General multiple inheritance has problems. Suppose that we have a type T with some components and operations. Perhaps

  1        type T is tagged
  2                record
  3                        A : Integer;
  4                        B : Boolean;
  5                end record;
  6        procedure Op1 (X : T);
  7        procedure Op2 (X : T);

@ Now suppose we derive two new types from T thus

  1        type T1 is new T with
  2                record
  3                        C : Character;
  4                end record;
  5        procedure Op3 (X : T1);
  6        -- Op1 and Op2 inherited, Op3 added
  7
  8        type T2 is new T with
  9                record
 10                        C : Colour;
 11                end record;
 12        procedure Op1 (X : T2);
 13        procedure Op4 (X : T2);
 14        -- Op1 overridden, Op2 inherited, Op4 added

@ Now suppose that we were able to derive a further type from both T1 and T2 by perhaps writing

  1        type TT is new T1 and T2 with null record; -- illegal

@ This is about the simplest example one could imagine. We have added no further components or operations. But what would TT have inherited from its two parents? There is a general rule that a record cannot have two components with the same identifier so presumably it has just one component A and one component B. But what about C? Does it inherit the character or the colour? Or is it illegal because of the clash? Suppose T2 had a component D instead of C. Would that be OK? Would TT then have four components? And then consider the operations. Presumably it has both Op1 and Op2. But which implementation of Op1? Is it the original Op1 inherited from T via T1 or the overridden version inherited from T2? Clearly it cannot have both. But there is no reason why it cannot have both Op3 and Op4, one inherited from each parent.

@ The problems arise when inheriting components from more than one parent and inheriting different implementations of the same operation from more than one parent. There is no problem with inheriting the same specification of an operation from two parents.

@ These observations provide the essence of the solution. At most one parent can have components and at most one parent can have concrete operations – for simplicity we make them the same parent. But abstract operations can be inherited from several parents. This can be phrased as saying that this kind of multiple inheritance is about merging contracts to be satisfied rather than merging algorithms or state.

@ So Ada 2005 introduces the concept of an interface which is a tagged type with no components and no concrete operations. The idea of a null procedure as an operation of a tagged type is also introduced; this has no body but behaves as if it has a null body. Interfaces are only permitted to have abstract subprograms and null procedures as operations.

@ We will outline the ways in which interfaces can be declared and composed in a symbolic way and then conclude with a more practical example.

@ We might declare a package Pi1 containing an interface Int1 thus

  1        package Pi1 is
  2                type Int1 is interface;
  3                procedure Op1 (X : Int1) is abstract;
  4                procedure N1 (X : Int1) is null;
  5        end Pi1;

@ Note the syntax. It uses the new reserved word interface. It does not say tagged although all interface types are tagged. The abstract procedure Op1 has to be explicitly stated to be abstract as usual. The null procedure N1 uses new syntax as well. Remember that a null procedure behaves as if its body comprises a single null statement; but it doesn't actually have a concrete body.

@ The main type derivation rule then becomes that a tagged type can be derived from zero or one conventional tagged types plus zero or more interface types. Thus

  1        type NT is new T and Int1 and Int2 with ... ;

@ where Int1 and Int2 are interface types. The normal tagged type if any has to be given first in the declaration. The first type is known as the parent so the parent could be a normal tagged type or an interface. The other types are known as progenitors. Additional components and operations are allowed in the usual way.

@ The term progenitors may seem strange but the term ancestors in this context was confusing and so a new term was necessary. Progenitors comes from the Latin progignere, to beget, and so is very appropriate.

@ It might have been thought that it would be quite feasible to avoid the formal introduction of the concept of an interface by simply saying that multiple parents are allowed provided only the first has components and concrete operations. However, there would have been implementation complexities with the risk of violating privacy and distributed overheads. Moreover, it would have caused maintenance problems since simply adding a component to a type or making one of its abstract operations concrete would cause errors elsewhere in the system if it was being used as a secondary parent. It is thus much better to treat interfaces as a fundamentally new concept. Another advantage is that this provides a new class of generic parameter rather neatly without complex rules for instantiations.

@ If the normal tagged type T is in a package Pt with operations Opt1, Opt2 and so on we could now write

  1        with Pi1, Pt;
  2        package PNT is
  3                type NT is new Pt.T and Pi1.Int1 with ... ;
  4                procedure Op1 (X : NT);   -- concrete procedure
  5                --  possibly other ops of NT
  6        end PNT;

@ We must of course provide a concrete procedure for Op1 inherited from the interface Int1 since we have declared NT as a concrete type. We could also provide an overriding for N1 but if we do not then we simply inherit the null procedure of Int1. We could also override the inherited operations Opt1 and Opt2 from T in the usual way.

@ Interfaces can be composed from other interfaces thus

  1        type Int2 is interface;
  2        ...
  3        type Int3 is interface and Int1;
  4        ...
  5        type Int4 is interface and Int1 and Int2;
  6        ...

@ Note the syntax. A tagged type declaration always has just one of interface, tagged and with (it doesn't have any if it is not a tagged type). When we derive interfaces in this way we can add new operations so that the new interface such as Int4 will have all the operations of both Int1 and Int2 plus possibly some others declared specifically as operations of Int4. All these operations must be abstract or null and there are fairly obvious rules regarding what happens if two or more of the ancestor interfaces have the same operation. Thus a null procedure overrides an abstract one but otherwise repeated operations must have profiles that are type conformant and have the same convention.

@ We refer to all the interfaces in an interface list as progenitors. So Int1 and Int2 are progenitors of Int4. The first one is not a parent – that term is only used when deriving a type as opposed to composing an interface.

@ Note that the term ancestor covers all generations whereas parent and progenitors are first generation only.

@ Similar rules apply when a tagged type is derived from another type plus one or more interfaces as in the case of the type NT which was

  1        type NT is new T and Int1 and Int2 with ... ;

@ In this case it might be that T already has some of the operations of Int1 and/or Int2. If so then the operations of T must match those of Int1 or Int2 (be type conformant etc).

@ We informally speak of a specific tagged type as implementing an interface from which it is derived (directly or indirectly). The phrase "implementing an interface" is not used formally in the definition of Ada 2005 but it is useful for purposes of discussion.

@ Thus in the above example the tagged type NT must implement all the operations of the interfaces Int1 and Int2. If the type T already implements some of the operations then the type NT will automatically implement them because it will inherit the implementations from T. It could of course override such inherited operations in the usual way.

@ The normal "going abstract" rules apply in the case of functions. Thus if one operation is a function F thus

  1        package Pi2 is
  2                type Int2 is interface;
  3                function F (Y : Int2) return Int2 is abstract;
  4        end Pi2;

@ and T already has such a conforming operation

  1        package PT is
  2                type T is tagged record ...
  3                function F (X : T) return T;
  4        end PT;

@ then in this case the type NT must provide a concrete function F. See however the discussion at the end of this paper for the case when the type NT has a null extension.

@ Class wide types also apply to interface types. The class wide type Int1'Class covers all the types derived from the interface Int1 (both other interfaces as well as normal tagged types). We can then dispatch using an object of a concrete tagged type in that class in the usual way since we know that any abstract operation of Int1 will have been overridden. So we might have

  1        type Int1_Ref is access all Int1'Class;
  2        NT_Var : aliased NT;
  3        Ref : Int1_Ref := NT_Var'Access;

@ Observe that conversion is permitted between the access to class wide type Int1_Ref and any access type that designates a type derived from the interface type Int1.

@ Interfaces can also be used in private extensions and as generic parameters.

@ Thus

  1                type PT is new T and Int2 and Int3 with private;
  2                ...
  3        private
  4                type PT is new T and Int2 and Int3 with null record;

@ An important rule regarding private extensions is that the full view and the partial view must agree with respect to the set of interfaces they implement. Thus although the parent in the full view need not be T but can be any type derived from T, the same is not true of the interfaces which must be such that they both implement the same set exactly. This rule is important in order to prevent a client type from overriding private operations of the parent if the client implements an interface added in the private part.

@ Generic parameters take the form

  1        generic
  2                type FI is interface and Int1 and Int2;
  3        package ...

@ and then the actual parameter must be an interface which implements all the ancestors Int1, Int2 etc. The formal could also just be type FI is interface; in which case the actual parameter can be any interface. There might be subprograms passed as further parameters which would require that the actual has certain operations. The interfaces Int1 and Int2 might themselves be formal parameters occurring earlier in the parameter list.

@ Interfaces (and formal interfaces) can also be limited thus type LI is limited interface; We can compose mixtures of limited and nonlimited interfaces but if any one of them is nonlimited then the resulting interface must not be specified as limited. This is because it must implement the equality and assignment operations implied by the nonlimited interface. Similar rules apply to types which implement one or more interfaces. We will come back to this topic in a moment.

@ There are other forms of interfaces, namely synchronized interfaces, task interfaces, and protected interfaces. These bring support for polymorphic, class wide object oriented programming to the real time programming arena. They will be described in a later paper.

@ Having described the general ideas in somewhat symbolic terms, we will now discuss a more concrete example.

@ Before doing so it is important to emphasize that interfaces cannot have components and therefore if we are to perform multiple inheritance then we should think in terms of abstract operations to read and write components rather than the components themselves. This is standard OO thinking anyway because it preserves abstraction by hiding implementation details.

@ Thus rather than having a component such as Comp it is better to have a pair of operations. The function to read the component can simply be called Comp. A procedure to update the component might be Set_Comp. We will generally use this convention although it is not always appropriate to treat the components as unrelated entities.

@ Suppose now that we want to print images of the geometrical objects. We will assume that the root type is declared as

  1        package Geometry is
  2                type Object is abstract tagged private;
  3                procedure Move (O : in out Object'Class; X, Y : Float);
  4                ...
  5        private
  6                type Object is abstract tagged
  7                        record
  8                                X_Coord : Float := 0.0;
  9                                Y_Coord : Float := 0.0;
 10                        end record;
 11                ...
 12        end;

@ The type Object is private and by default both coordinates have the value of zero. The procedure Move, which is class wide, enables any object to be moved to the location specified by the parameters.

@ Suppose also that we have a line drawing package with the following specification

  1        package Line_Draw is
  2                type Printable is interface;
  3                type Colour is ... ;
  4                type Points is ... ;
  5                procedure Set_Hue (P : in out Printable; C : in Colour) is abstract;
  6                function Hue (P : Printable) return Colour is abstract;
  7                procedure Set_Width (P : in out Printable; W : in Points) is abstract;
  8                function Width (P : Printable) return Points is abstract;
  9                type Line is ... ;
 10                type Line_Set is ... ;
 11                function To_Lines (P : Printable) return Line_Set is abstract;
 12                procedure Print (P : in Printable'Class);
 13        private
 14                procedure Draw_It (L : Line; C : Colour; W : Points);
 15        end Line_Draw;

@ The idea of this package is that it enables the drawing of an image as a set of lines. The attributes of the image are the hue and the width of the lines and there are pairs of subprograms to set and read these properties of any object of the interface Printable and its descendants. These operations are of course abstract.

@ In order to prepare an object in a form that can be printed it has to be converted to a set of lines. The function To_Lines converts an object of the type Printable into a set of lines; again it is abstract. The details of various types such as Line and Line_Set are not shown.

@ Finally the package Line_Draw declares a concrete procedure Print which takes an object of type Printable'Class and does the actual drawing using the slave procedure Draw_It declared in the private part. Note that Print is class wide and is concrete. This is an important point. Although all primitive operations of an interface must be abstract this does not apply to class wide operations since these are not primitive.

@ The body of the procedure Print could take the form

  1        procedure Print (P : in Printable'Class) is
  2                L      : Line_Set := To_Lines (P);
  3                A_Line : Line;
  4        begin
  5                loop
  6                        -- iterate over the Line_Set and extract each line
  7                        A_Line := ...
  8                        Draw_It (A_Line, Hue (P), Width (P));
  9                end loop;
 10        end Print;

@ but this is all hidden from the user. Note that the procedure Draw_It is declared in the private part since it need not be visible to the user.

@ One reason why the user has to provide To_Lines is that only the user knows about the details of how best to represent the object. For example the poor circle will have to be represented crudely as a polygon of many sides, perhaps a hectogon of 100 sides.

@ We can now take at least two different approaches. We can for example write

  1        with Geometry, Line_Draw;
  2        package Printable_Geometry is
  3                type Printable_Object is
  4                abstract new Geometry.Object and Line_Draw.Printable with private;
  5                procedure Set_Hue (P : in out Printable_Object; C : in Colour);
  6                function Hue (P : Printable_Object) return Colour;
  7                procedure Set_Width (P : in out Printable_Object; W : in Points);
  8                function Width (P : Printable_Object) return Points;
  9                function To_Lines (P : Printable_Object) return Line_Set is abstract;
 10        private
 11                ...
 12        end Printable_Geometry;

@ The type Printable_Object is a descendant of both Object and Printable and all concrete types descended from Printable_Object will therefore have all the operations of both Object and Printable. Note carefully that we have to put Object first in the declaration of Printable_Object and that the following would be illegal

  1        type Printable_Object is
  2                abstract new Line_Draw.Printable and Geometry.Object with private; --illegal

@ This is because of the rule that only the first type in the list can be a normal tagged type; any others must be interfaces. Remember that the first type is always known as the parent type and so the parent type in this case is Object.

@ The type Printable_Object is declared as abstract because we do not want to implement To_Lines at this stage. Nevertheless we can provide concrete subprograms for all the other operations of the interface Printable. We have given the type a private extension and so in the private part of its containing package we might have

  1        private
  2                type Printable_Object
  3                        is abstract new Geometry.Object and Line_Draw.Printable
  4                                with record
  5                                        Hue   : Colour := Black;
  6                                        Width : Points := 1;
  7                                end record;
  8        end Printable_Geometry;

@ Just for way of illustration, the components have been given default values. In the package body the operations such as the function Hue are simply

  1        function Hue (P : Printable_Object) return Colour is
  2        begin
  3                return P.Hue;
  4        end;

@ Luckily the visibility rules are such that this does not do an infinite recursion! Note that the information containing the style components is in the record structure following the geometrical properties. This is a simple linear structure since interfaces cannot add components. However, since the type Printable_Object has all the operations of both an Object and a Printable, this adds a small amount of complexity to the arrangement of dispatch tables. But this detail is hidden from the user.

@ The key point is that we can now pass any object of the type Printable_Object or its descendants to the procedure

  1        procedure Print (P : in Printable'Class);

@ and then (as outlined above) within Print we can find the colour to be used by calling the function Hue and the line width to use by calling the function Width and we can convert the object into a set of lines by calling the function To_Lines.

@ And now we can declare the various types Circle, Triangle, Square and so on by making them descendants of the type Printable_Object and in each case we have to implement the function To_Lines.

@ The unfortunate aspect of this approach is that we have to move the geometry hierarchy. For example the triangle package might now be

  1        package Printable_Geometry.Triangles is
  2                type Printable_Triangle is new Printable_Object
  3                        with record
  4                                A, B, C : Float;
  5                        end record;
  6                ... -- functions Area, To_Lines etc
  7        end;

@ We can now declare a Printable_Triangle thus

  1        A_Triangle : Printable_Triangle := (Printable_Object with A => 4.0, B => 4.0, C => 4.0);

@ This declares an equilateral triangle with sides of length 4.0. Its private Hue and Width components are set by default. Its coordinates which are also private are by default set to zero so that it is located at the origin. (The reader can improve the example by making the components A, B and C private as well.) We can conveniently move it to wherever we want by using the procedure Move which being class wide applies to all types derived from Object. So we can write

  1        A_Triangle.Move (1.0, 2.0);

@ And now we can make a red sign

  1        Sign: Printable_Triangle := A_Triangle;

@ Having declared the object Sign, we can give it width and hue and print it

  1        Sign.Set_Hue (Red);
  2        Sign.Set_Width (3);
  3        Sign.Print; -- print thick red triangle

@ As we observed earlier this approach has the disadvantage that we had to move the geometry hierarchy. A different approach which avoids this is to declare printable objects of just the kinds we want as and when we want them.

@ So assume now that we have the package Line_Draw as before and the original package Geometry and its child packages. Suppose we want to make printable triangles and circles. We could write

  1        with Geometry, Line_Draw; use Geometry;
  2        package Printable_Objects is
  3                type Printable_Triangle is new Triangles.Triangle and Line_Draw.Printable with private;
  4                type Printable_Circle is new Circles.Circle and Line_Draw.Printable with private;
  5                procedure Set_Hue (P : in out Printable_Triangle; C : in Colour);
  6                function Hue (P : Printable_Triangle return Colour;
  7                procedure Set_Width (P : in out Printable_Triangle; W : in Points);
  8                function Width (P : Printable_Triangle) return Points;
  9                function To_Lines (T : Printable_Triangle) return Line_Set;
 10                procedure Set_Hue (P : in out Printable_Circle; C : in Colour);
 11                function Hue (P : Printable_Circle) return Colour;
 12                procedure Set_Width (P : in out Printable_Circle; W : in Points);
 13                function Width (P : Printable_Circle) return Points;
 14                function To_Lines (C : Printable_Circle) return Line_Set;
 15        private
 16                type Printable_Triangle
 17                        is new Triangles.Triangle and Line_Draw.Printable
 18                                with record
 19                                        Hue   : Colour := Black;
 20                                        Width : Points := 1;
 21                                end record;
 22                type Printable_Circle
 23                        is new Circles.Circle and Line_Draw.Printable
 24                                with record
 25                                        Hue   : Colour := Black;
 26                                        Width : Points := 1;
 27                                end record;
 28        end Printable_Objects;

@ and the body of the package will provide the various subprogram bodies.

@ Now suppose we already have a normal triangle thus

  1        A_Triangle : Geometry.Triangles.Triangle := ... ;

@ In order to print A_Triangle we first have to declare a printable triangle thus

  1        Sign : Printable_Triangle;

@ and now we can set the triangle components of it using a view conversion thus

  1        Triangle (Sign) := A_Triangle;

@ And then as before we write

  1        Sign.Set_Hue (Red);
  2        Sign.Set_Width (3);
  3        Sign.Print_It; -- print thick red triangle

@ This second approach is probably better since it does not require changing the geometry hierarchy. The downside is that we have to declare the boring hue and width subprograms repeatedly. We can make this much easier by declaring a generic package thus

  1        with Line_Draw;  use Line_Draw;
  2        generic
  3                type T is abstract tagged private;
  4        package Make_Printable is
  5                type Printable_T is abstract new T and Printable with private;
  6                procedure Set_Hue (P : in out Printable_T; C : in Colour);
  7                function Hue (P : Printable_T) return Colour;
  8                procedure Set_Width (P : in out Printable_T; W : in Points);
  9                function Width (P : Printable_T) return Points;
 10        private
 11                type Printable_T
 12                        is abstract new T and Printable
 13                                with record
 14                                        Hue   : Colour := Black;
 15                                        Width : Points := 1;
 16                                end record;
 17        end;

@ This generic can be used to make any type printable. We simply write

  1        package P_Triangle is new Make_Printable (Triangle);
  2        type Printable_Triangle is new P_Triangle.Printable_T with null record;
  3        function To_Lines (T : Printable_Triangle) return Line_Set;

@ The instantiation of the package creates a type Printable_T which has all the hue and width operations and the required additional components. However, it simply inherits the abstract function To_Lines and so itself has to be an abstract type. Note that the function To_Lines has to be especially coded for each type anyway unlike the hue and width operations which can be the same.

@ We now do a further derivation largely in order to give the type Printable_T the required name Printable_Triangle and at this stage we provide the concrete function To_Lines.

@ We can then proceed as before. Thus the generic makes the whole process very easy – any type can be made printable by just writing three lines plus the body of the function To_Lines.

@ Hopefully this example has illustrated a number of important points about the use of interfaces. The key thing perhaps is that we can use the procedure Print to print anything that implements the interface Printable.

@ Earlier we stated that it was a common convention to provide pairs of operations to read and update properties such as Hue and Set_Hue and Width and Set_Width. This is not always appropriate. Thus if we have related components such as X_Coord and Y_Coord then although individual functions to read them might be sensible, it is undoubtedly better to update the two values together with a single procedure such as the procedure Move declared earlier. Thus if we wish to move an object from the origin (0.0, 0.0) to say (3.0, 4.0) and do it by two calls

  1        Obj.Set_X_Coord (3.0); -- first change X
  2        Obj.Set_Y_Coord (4.0); -- then change Y

@ then it seems as if it was transitorily at the point (3.0, 0.0). There are various other risks as well. We might forget to set one component or accidentally set the same component twice.

@ Finally, as discussed earlier, null procedures are a new kind of subprogram and the user-defined operations of an interface must be null procedures or abstract subprograms – there is of course no such thing as a null function. (Nonlimited interfaces do have one concrete operation and that is predefined equality; it could even be overridden with an abstract one.) Null procedures will be found useful for interfaces but are in fact applicable to any types. As an example the package Ada.Finalization now uses null procedures for Initialize, Adjust, and Finalize as described in the Introduction.

@ We conclude this section with a few further remarks on limitedness. We noted earlier that an interface can be explicitly stated to be limited so we might have

  1        type LI is limited interface; -- limited
  2        type NLI is interface;        -- nonlimited

@ An interface is limited only if it says limited (or synchronized etc). As mentioned earlier, a descendant of a nonlimited interface must be nonlimited since it must implement assignment and equality. So if an interface is composed from a mixture of limited and nonlimited interfaces it must be nonlimited

  1        type I is interface and LI and NLI;         -- legal
  2        type I is limited interface and LI and NLI; -- illegal

@ In other words, limitedness is never inherited from an interface but has to be stated explicitly. This applies to both the composition of interfaces and type derivation. On the other hand, in the case of type derivation, limitedness is inherited from the parent provided it is not an interface. This is necessary for compatibility with Ada 95. So given

  1        type LT is limited tagged ...
  2        type NLT is tagged ...

@ then

  1        type T is new NLT and LI  with ... -- legal, T not limited
  2        type T is new NLT and NLI with ... -- legal, T not limited
  3        type T is new LT  and LI  with ... -- legal, T limited
  4        type T is new LT  and NLI with ... -- illegal

@ The last is illegal because T is expected to be limited because it is derived from the limited parent type LT and yet it is also a descendant of the nonlimited interface NLI.

@ In order to avoid certain curious difficulties, Ada 2005 permits limited to be stated explicitly on type derivation. (It would have been nice to insist on this always for clarity but such a change would have been too much of an incompatibility.) If we do state limited explicitly then the parent must be limited (whether it is a type or an interface).

@ Using limited is necessary if we wish to derive a limited type from a limited interface thus

  1        type T is limited new LI with ...

@ These rules really all come down to the same thing. If a parent or progenitor (indeed any ancestor) is nonlimited then the descendant must be nonlimited. We can state that in reverse, if a type (including an interface) is limited then all its ancestors must be limited.

@ An earlier version of Ada 2005 ran into difficulties in this area because in the case of a type derived just from interfaces, the behaviour could depend upon the order of their appearance in the list (because the rules for parent and progenitors are a bit different). But in the final version of the language the order does not matter. So

  1        type T is new NLI and LI with ... -- legal, not limited
  2        type T is new LI and NLI with ... -- legal, not limited

@ But the following are of course illegal

  1        type T is limited new NLI and LI with ... -- illegal
  2        type T is limited new LI and NLI with ... -- illegal

@ There are also similar changes to generic formals and type extension – Ada 2005 permits limited to be given explicitly in both cases.

Rationale for Ada 2005: Object oriented model

@ENGRUSTOPBACKNEXT

4. Интерфейсы

@ В Аде 95 дочерний тип может иметь только одного непосредственного предка. Это означает, что истинное многократное наследование не возможно, хотя любопытные методики с использованием дискриминантов и настраиваемых средств могут использоваться при определённых обстоятельствах, но тем не менее, многократное наследование имеет проблемы. Предположим, что мы имеем тип T с некоторыми компонентами и операциями:

  1        type T is tagged
  2                record
  3                        A : Integer;
  4                        B : Boolean;
  5                end record;
  6        procedure Op1 (X : T);
  7        procedure Op2 (X : T);

@ Предположим, что мы получаем два новых типа из T следующим образом:

  1        type T1 is new T with
  2                record
  3                        C : Character;
  4                end record;
  5        procedure Op3 (X : T1);
  6        -- Op1 and Op2 inherited, Op3 added
  7
  8        type T2 is new T with
  9                record
 10                        C : Colour;
 11                end record;
 12        procedure Op1 (X : T2);
 13        procedure Op4 (X : T2);
 14        -- Op1 overridden, Op2 inherited, Op4 added

@ Теперь предположим, что мы могли бы получить дальнейший тип TT из T1 и T2:

  1        type TT is new T1 and T2 with null record; -- illegal

@ Это ещё самый простой пример, который можно было бы вообразить. Мы не добавили никаких дополнительных компонентов или операций. Но что ТТ унаследовал бы от своих двух родителей? Ясно, что запись не может иметь две компоненты с одинаковыми идентификаторами, Она имеет только одну компоненту A и одну компоненту B. Но что относительно C? Наследуется символ или цвет? И действительно ли это незаконно из-за противоречий? Предположим, что T2 имел бы компоненту D вместо C. Всё при этом было бы в порядке? ТТ тогда имел бы четыре компоненты? Теперь рассмотрим операции. По-видимому, TT имеет и Op1 и Op2. Но какую из Op1? Действительно ли это - первоначальная Op1 унаследованная из T через T1 или заменённая (overriding) версия унаследованная из T2? Ясно это TT не может иметь их обоих. Но нет никакой причины почему он не может иметь и Op3 и Op4, унаследованные каждая от своего родителя.

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

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

@ Так Ада 2005 вводит понятие интерфейса, который является теговым типом без компонентов и конкретных операций. Также вводится понятие пустой процедуры как операции тегового типа; такая процедура не имеет никакого тела, но ведет себя, как будто имеет тело с единственным оператором null. Таким образом, интерфейсам разрешают иметь только абстрактные подпрограммы и пустые процедуры.

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

@ Объявим пакет Pi1 содержащий интерфейс Int1:

  1        package Pi1 is
  2                type Int1 is interface;
  3                procedure Op1 (X : Int1) is abstract;
  4                procedure N1 (X : Int1) is null;
  5        end Pi1;

@ Обратите внимание на синтаксис применения нового зарезервированного слова interface. Здесь не указывается ключевое слово tagged, хотя все типы интерфейса теговые. Процедура Op1 явно объявлена абстрактной как её и положено. Пустая процедура N1 также использует новый синтаксис. Напомним, что пустая процедура ведет себя, как будто её тело включает единственное null утверждение; но она фактически не имеет конкретного тела.

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

  1        type NT is new T and Int1 and Int2 with ... ;

@ где Int1 и Int2 - интерфейсные типы. Нормальный теговый тип, если он имеется, должен быть указан на первом месте в объявлении. Первый тип считается родителем, таким образом, родитель может быть обычным теговым типом или интерфейсом. Другие типы считаются прародителями. Дополнительные компоненты и операции наследуются обычным способом.

@ Термин прародители (progenitors) может показаться странным, но термин предки (ancestors) в этом контексте тоже был бы ещё более запутывающим. Но новый термин был всё же необходим. Термин 'прародители' происходит из латинского слова 'progignere' - порождать, что весьма отражает суть обозначаемого явления.

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

@ Если нормальный теговый тип T, находится в пакете Pt с операциями Opt1, Opt2 и так далее, тогда мы можем бы написать:

  1        with Pi1, Pt;
  2        package PNT is
  3                type NT is new Pt.T and Pi1.Int1 with ... ;
  4                procedure Op1 (X : NT);   -- concrete procedure
  5                --  possibly other ops of NT
  6        end PNT;

@ Мы конечно, должны обеспечить конкретную процедуру для Op1, унаследованную от интерфейса Int1, так как мы объявили NT как конкретный тип. Мы могли также обеспечить замену (overriding) для N1, но если мы не делаем это, тогда мы просто наследуем пустую процедуру Int1. Мы могли также заменить (override) унаследованные из T операции Opt1 и Opt2 обычным способом.

@ Интерфейсы могут быть составлены из других интерфейсов следующим образом:

  1        type Int2 is interface;
  2        ...
  3        type Int3 is interface and Int1;
  4        ...
  5        type Int4 is interface and Int1 and Int2;
  6        ...

@ Рассмотрим синтаксис. Объявление тегового типа имеет атрибут interface либо tagged with (в противном случае это не теговый тип). Когда мы наследуем интерфейсы таким образом мы можем добавить новые операции так, что новый интерфейс Int4 будет иметь все операции из Int1 и Int2 плюс возможно объявленные операции непосредственно в Int4. Все эти операции должны быть абстрактными или пустыми, и есть довольно очевидные правила относительно того, что случается, если имеются два или больше интерфейса имеющих одну и ту же операцию. Таким образом, пустая процедура заменяет абстрактную, в противном случае одинаковые операции должны иметь совместимые конфигурации.

@ Мы ссылаемся ко всем интерфейсам в интерфейсном списке как на прародителей. Таким образом Int1 и Int2 - прародители Int4. Не родители, ибо этот термин используется только при получении типа, а не интерфейса.

@ Заметим, что термин предок охватывает все поколения, тогда как родитель и прародители - только первое поколение.

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

  1        type NT is new T and Int1 and Int2 with ... ;

@ В этом случае могло бы быть, что T уже имеет некоторые операции из Int1 и/или Int2. Если так, тогда операции T должны соответствовать таковым из Int1 или Int2 (быть совместимого типа и т.д).

@ Мы неофициально говорим об определенном теговом типе как о осуществлении интерфейса, из которого это унаследовано (прямо или косвенно). Фраза "осуществление интерфейса" не используется официально в определении Ады 2005, но это полезно в целях обсуждения.

@ Таким образом, в вышеупомянутом примере теговый тип NT должен реализовать все операции интерфейсов Int1 и Int2. Если тип T уже реализует некоторые из операций, тогда тип NT реализует их автоматически, потому что он унаследует реализацию от T. Он может конечно заменить такие унаследованные операции обычным способом.

@ Нормальные "go abstract" правила применяется в случае функций. Таким образом, если одна операция - функция F то:

  1        package Pi2 is
  2                type Int2 is interface;
  3                function F (Y : Int2) return Int2 is abstract;
  4        end Pi2;

@ и T уже имеет подобную операцию:

  1        package PT is
  2                type T is tagged record ...
  3                function F (X : T) return T;
  4        end PT;

@ тогда в этом случае тип NT должен обеспечить конкретную функцию F. См. однако обсуждение в конце этой статьи для случая когда тип NT имеет нулевое расширение.

@ Надклассовые типы также применяется к интерфейсным типам. Надклассовый тип Int1'Class покрывает все типы, унаследованные из интерфейса Int1 (и другие интерфейсы, такие как нормальные теговые типы). Мы можем тогда использовать объект конкретного тегового типа в том классе обычным способом, так как мы знаем, что любая абстрактная операция Int1 будет заменена (overridden). Таким образом, мы могли бы иметь:

  1        type Int1_Ref is access all Int1'Class;
  2        NT_Var : aliased NT;
  3        Ref : Int1_Ref := NT_Var'Access;

@ Заметим, что преобразование разрешается между ссылкой на надклассовый тип Int1_Ref и любым ссылочным типом, который наследуется из интерфейсного типа Int1.

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

@ Таким образом:

  1                type PT is new T and Int2 and Int3 with private;
  2                ...
  3        private
  4                type PT is new T and Int2 and Int3 with null record;

@ Важное правило относительно приватных секций состоит в том, что полное представление и частичное представление должны соответствовать набору интерфейсов, которые они осуществляют. Таким образом, хотя родитель в полном представлении не должен быть T, но может быть любым типом, унаследованным из T, то же самое не верно для интерфейсов, которые оба должны осуществлять один и тот же набор. Это правило необходимо для воспрепятствования клиентским типам заменять приватные операции родителя, если клиент осуществляет интерфейс добавленный в приватной секции.

@ Настраиваемые параметры принимают форму:

  1        generic
  2                type FI is interface and Int1 and Int2;
  3        package ...

@ и затем фактический параметр должен быть интерфейсом, который реализует всех предков Int1, Int2 и т.д. Формальным мог быть только тип FI являющийся интерфейсом; когда как фактический параметр может быть любым интерфейсом. Мог бы быть подпрограммами, которые передают как дальнейшие параметры, которые будут требовать, чтобы фактические имели определенные операции. Интерфейсы Int1 и Int2 могли бы непосредственно быть формальными параметрами, описанные ранее в списке параметров.

@ Интерфейсы (и формальные интерфейсы) могут также быть ограничеными (это обозначается таким образом: type LI is limited interface;) Мы можем составить целый набор из ограниченных и неограниченных интерфейсов, но если хотя бы один из них неограничен, тогда получающийся интерфейс не должен быть определен как ограниченный. Это потому что должны осуществляться операции равенства и назначения, подразумеваемые неограниченным интерфейсом. Подобные правила относятся к типам, которые осуществляют один или более интерфейсов. Мы возвратимся к этой теме через мгновение.

@ Есть другие формы интерфейсов, а именно, синхронизированные (synchronized) интерфейсы, интерфейсы задач, и защищенные (protected) интерфейсы. Они поддерживают полиморфизм, надклассовое объектно-ориентированное программирование в реальном времени. Они будут описаны в позже.

@ Описав общие идеи в символических терминах, мы теперь обсудим более конкретный пример.

@ Перед тем как мы начнём, важно подчеркнуть, что интерфейсы не могут иметь компоненты и поэтому если мы должны выполнить многократное наследование, тогда мы должны думать в терминах абстрактных операций чтения и записи компонентов, а не о компонентах непосредственно. Это - стандарт OOП мышления, потому что это сохраняет абстракцию, скрывая детали выполнения.

@ Таким образом, вместо того, чтобы иметь компоненту типа Comp лучше иметь пару операций. Функцию для чтения компоненты можно просто назвать Comp. Процедура для обновления компоненты могла бы называться Set_Comp. Мы будем в основном использовать это соглашение, хотя не всегда правильно обрабатывать компоненты как несвязанные объекты.

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

  1        package Geometry is
  2                type Object is abstract tagged private;
  3                procedure Move (O : in out Object'Class; X, Y : Float);
  4                ...
  5        private
  6                type Object is abstract tagged
  7                        record
  8                                X_Coord : Float := 0.0;
  9                                Y_Coord : Float := 0.0;
 10                        end record;
 11                ...
 12        end;

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

@ Предположим также, что мы имеем пакет для рисования со следующей спецификацией:

  1        package Line_Draw is
  2                type Printable is interface;
  3                type Colour is ... ;
  4                type Points is ... ;
  5                procedure Set_Hue (P : in out Printable; C : in Colour) is abstract;
  6                function Hue (P : Printable) return Colour is abstract;
  7                procedure Set_Width (P : in out Printable; W : in Points) is abstract;
  8                function Width (P : Printable) return Points is abstract;
  9                type Line is ... ;
 10                type Line_Set is ... ;
 11                function To_Lines (P : Printable) return Line_Set is abstract;
 12                procedure Print (P : in Printable'Class);
 13        private
 14                procedure Draw_It (L : Line; C : Colour; W : Points);
 15        end Line_Draw;

@ Идея этого пакета состоит в том, чтобы представить изображение как набор линий. Атрибуты изображения - оттенок и ширина линий и есть пара подпрограмм чтобы установить и читать эти свойства любого объекта интерфейса Printable и его потомков. Эти операции конечно абстрактны.

@ Чтобы привести объект к форме, которая может быть напечатана, он должен быть преобразован в набор линий. Функция To_Lines преобразовывает объект типа Printable в набор линий; она также является абстрактной. Детали различных типов, типа Line и Line_Set не показаны.

@ Наконец, пакет Line_Draw объявляет конкретную процедуру Print, которая берет объект типа Printable'Class и делает фактический рисунок, используя подчиненную процедуру Draw_It объявленную в частной секции. Отметим, что процедура Print - надклассовая и конкретная. Это - важный момент. Хотя все примитивные операции интерфейса должны быть абстрактными, это не относится к надклассовым операциям, так как они не примитивные.

@ Тело процедуры Print могло бы иметь вид:

  1        procedure Print (P : in Printable'Class) is
  2                L      : Line_Set := To_Lines (P);
  3                A_Line : Line;
  4        begin
  5                loop
  6                        -- iterate over the Line_Set and extract each line
  7                        A_Line := ...
  8                        Draw_It (A_Line, Hue (P), Width (P));
  9                end loop;
 10        end Print;

@ но всё это скрыто от пользователя. Отметим, что процедура Draw_It объявлена в частной части, так как она не должна быть видимой пользователю.

@ Одна из причин, почему пользователь должен обеспечить процедуру To_Lines состоит в том, что только пользователь знает о деталях как лучше всего представить объект. Например, круг может быть представлен грубо как многоугольник из 100 сторон.

@ Мы можем иметь по крайней мере два различных подхода. Например, мы можем написать:

  1        with Geometry, Line_Draw;
  2        package Printable_Geometry is
  3                type Printable_Object is
  4                        abstract new Geometry.Object and Line_Draw.Printable with private;
  5                procedure Set_Hue (P : in out Printable_Object; C : in Colour);
  6                function Hue (P : Printable_Object) return Colour;
  7                procedure Set_Width (P : in out Printable_Object; W : in Points);
  8                function Width (P : Printable_Object) return Points;
  9                function To_Lines (P : Printable_Object) return Line_Set is abstract;
 10        private
 11                ...
 12        end Printable_Geometry;

@ Тип Printable_Object является потомком и Object и Printable и все конкретные типы потомков Printable_Object будут поэтому иметь все операции и Object и Printable. Обратите внимание, что мы должны поместить Object сначала в объявлении Printable_Object и что следующее было бы незаконным:

  1        type Printable_Object is
  2                abstract new Line_Draw.Printable and Geometry.Object with private; --illegal

@ Это потому, что только первый тип в списке может быть нормальным теговым типом; любые другие должны быть интерфейсами. Напомним, что первый тип всегда известен как родительский и в этом случае здесь родительским типом является - Object.

@ Тип Printable_Object объявлен как абстрактный, потому что мы не хотим реализовать To_Lines на данном этапе. Однако мы можем обеспечить конкретные подпрограммы для всех других операций интерфейса Printable. Мы дали типу частное расширение, и в частной секции пакета мы могли бы иметь:

  1        private
  2                type Printable_Object
  3                        is abstract new Geometry.Object and Line_Draw.Printable
  4                                with record
  5                                        Hue   : Colour := Black;
  6                                        Width : Points := 1;
  7                                end record;
  8        end Printable_Geometry;

@ Только для иллюстрации компонентам дали значения по умолчанию. В теле пакета - операции типа функции Hue простые:

  1        function Hue (P : Printable_Object) return Colour is
  2        begin
  3                return P.Hue;
  4        end;

@ К счастью, правила видимости не позволяют бесконечной рекурсии! Отметим, что информация содержащая компоненты стиля находится в структуре записи после геометрических свойств. Это - простая линейная структура, так как интерфейсы не могут добавлять компоненты. Однако, начиная с типа Printable_Object имеет все операции и Object и Printable, это немного усложняет расположение таблиц отправки. Но эта деталь скрыта от пользователя.

@ Ключевой пункт состоит в том, что теперь мы можем передать любой объект типа Printable_Object или его потомков процедуре:

  1        procedure Print (P : in Printable'Class);

@ и затем (как выделено выше) в пределах Print мы можем определить цвет функцией Hue и ширину линии функцией Width, и мы можем преобразовать объект в ряд линий, вызывая функцию To_Lines.

@ И теперь мы можем объявить различные типы Circle, Triangle, Square и так далее, делая их потомками типа Printable_Object, и в каждом случае мы будем иметь соответствующую реализацию функции To_Lines.

@ Неудачный аспект этого подхода состоит в тоим, что мы должны будем нарушить геометрическую иерархию. Например, пакет треугольника мог бы теперь быть:

  1        package Printable_Geometry.Triangles is
  2                type Printable_Triangle is new Printable_Object
  3                        with record
  4                                A, B, C : Float;
  5                        end record;
  6                ... -- functions Area, To_Lines etc
  7        end;

@ Мы можем теперь объявить Printable_Triangle таким образом:

  1        A_Triangle : Printable_Triangle :=
  2                (Printable_Object with A => 4.0, B => 4.0, C => 4.0);

@ Здесь объявляется равносторонний треугольник со сторонами длинной 4.0. Его частные компоненты Hue и Width устанавливаются по умолчанию. Его координаты, которые также являются частными, по умолчанию устанавливаются в нуль. (Читатель может улучшить пример, делая компоненты A, B и C также частными.) Мы можем свободно переместить его в любое место процедурой Move, каторая будучи надклассового типа применяется ко всем типам, унаследованным из Object. Таким образом, мы можем написать:

  1        A_Triangle.Move (1.0, 2.0);

@ И теперь мы можем сделать красный признак

  1        Sign: Printable_Triangle := A_Triangle;

@ Объявив объект Sign, Мы можем назначить ему ширину и оттенок:

  1        Sign.Set_Hue (Red);
  2        Sign.Set_Width (3);
  3        Sign.Print; -- print thick red triangle

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

@ Предположим, что мы имеем пакет Line_Draw и первоначальный пакет Geometry и его дочерние пакеты. Предположим, что мы хотим сделать пригодные для печатания треугольники и круги. Мы могли бы написать:

  1        with Geometry, Line_Draw; use Geometry;
  2        package Printable_Objects is
  3                type Printable_Triangle is new Triangles.Triangle and Line_Draw.Printable with private;
  4                type Printable_Circle is new Circles.Circle and Line_Draw.Printable with private;
  5                procedure Set_Hue (P : in out Printable_Triangle; C : in Colour);
  6                function Hue (P : Printable_Triangle return Colour;
  7                procedure Set_Width (P : in out Printable_Triangle; W : in Points);
  8                function Width (P : Printable_Triangle) return Points;
  9                function To_Lines (T : Printable_Triangle) return Line_Set;
 10                procedure Set_Hue (P : in out Printable_Circle; C : in Colour);
 11                function Hue (P : Printable_Circle) return Colour;
 12                procedure Set_Width (P : in out Printable_Circle; W : in Points);
 13                function Width (P : Printable_Circle) return Points;
 14                function To_Lines (C : Printable_Circle) return Line_Set;
 15        private
 16                type Printable_Triangle
 17                        is new Triangles.Triangle and Line_Draw.Printable
 18                                with record
 19                                        Hue   : Colour := Black;
 20                                        Width : Points := 1;
 21                                end record;
 22                type Printable_Circle
 23                        is new Circles.Circle and Line_Draw.Printable
 24                                with record
 25                                        Hue   : Colour := Black;
 26                                        Width : Points := 1;
 27                                end record;
 28        end Printable_Objects;

@ и тело пакета обеспечит различные тела подпрограмм.

@ Теперь предположим, что мы уже имеем нормальный треугольник:

  1        A_Triangle : Geometry.Triangles.Triangle := ... ;

@ Чтобы напечатать A_Triangle, мы сначала должны объявить пригодный для печатания треугольник:

  1        Sign : Printable_Triangle;

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

  1        Triangle (Sign) := A_Triangle;

@ и теперь мы можем написать:

  1        Sign.Set_Hue (Red);
  2        Sign.Set_Width (3);
  3        Sign.Print_It; -- print thick red triangle

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

  1        with Line_Draw;  use Line_Draw;
  2        generic
  3                type T is abstract tagged private;
  4        package Make_Printable is
  5                type Printable_T is abstract new T and Printable with private;
  6                procedure Set_Hue (P : in out Printable_T; C : in Colour);
  7                function Hue (P : Printable_T) return Colour;
  8                procedure Set_Width (P : in out Printable_T; W : in Points);
  9                function Width (P : Printable_T) return Points;
 10        private
 11                type Printable_T
 12                        is abstract new T and Printable
 13                                with record
 14                                        Hue   : Colour := Black;
 15                                        Width : Points := 1;
 16                                end record;
 17        end;

@ Этот настраиваемый пакет может использоваться, чтобы сделать любой тип пригодным для печатания. Мы просто пишем:

  1        package P_Triangle is new Make_Printable (Triangle);
  2        type Printable_Triangle is new P_Triangle.Printable_T with null record;
  3        function To_Lines (T : Printable_Triangle) return Line_Set;

@ Реализация пакета создает тип Printable_T, который имеет все операции для оттенка и ширины и необходимые дополнительные компоненты. Однако, он просто наследует абстрактную функцию To_Lines и, таким образом, непосредственно должен быть абстрактным типом. Отметим, что функция To_Lines должна быть специально закодирована для каждого типа индивидуально кроме оттенка и операций ширины, которые могут быть одни на всех.

@ Теперь мы делаем дальнейшее преобразование чтобы дать типу Printable_T необходимое имя Printable_Triangle, и на данном этапе мы обеспечиваем конкретную функцию To_Lines.

@ Мы можем продолжать и далее. Таким образом, настраиваемые средства делают этот процесс очень простым - любой тип может быть сделан пригодным для печатания, только пишущий три линии плюс тело функции To_Lines.

@ Мы надеемся, что этот пример проиллюстрировал множество важных пунктов использования интерфейсов. Здесь ключевая вещь состоит в том, что мы можем использовать процедуру Print, для печати чего - нибудь через интерфейс Printable.

@ Ранее мы обеспечили пару операций, чтобы читать и обновлять свойства, типа Hue и Set_Hue и Width и Set_Width. Это не всегда приемлемо. Таким образом, если мы связали компоненты типа X_Coord и Y_Coord тогда, хотя вполне можно было бы обойтись индивидуальными функциями, несомненно лучше обновлять два значения вместе отдельной процедурой, типа процедуры Move, объявленной ранее. Таким образом, если мы желаем переместить объект из точки (0.0, 0.0) в точку (3.0, 4.0) то сделать это можно двумя вызовами:

  1        Obj.Set_X_Coord (3.0); -- first change X
  2        Obj.Set_Y_Coord (4.0); -- then change Y

@ тогда окажется, что он будто бы временно побывал в точке (3.0, 0.0). Здесь имеются и некоторые другие риски. Мы могли бы забыть устанавливать одну из компонент или случайно установить одну и ту же компоненту дважды.

@ Наконец, как обсуждалось ранее, пустые процедуры - новый вид подпрограмм, и определяемые пользователем операции интерфейса должны быть пустыми процедурами или абстрактными подпрограммами - нет конечно такой вещи как пустая функция. (Неограниченные интерфейсы действительно имеют одну конкретную операцию, и это - предопределенное равенство; это могло даже быть отменено с абстрактным.) Null процедуры будут найдены полезными для интерфейсов, но фактически применимы к любым типам. Как пример, пакет Ada.Finalization теперь использует пустые процедуры для Initialize, Adjust и Finalize как описано во Введении.

@ Мы заключаем этот раздел с несколькими дальнейшими замечаниями по ограниченности (limitedness). Мы отметили ранее, что интерфейс может быть явно заявлен как ограниченный, так мы могли бы иметь

  1        type LI is limited interface; -- limited
  2        type NLI is interface;        -- nonlimited

@ Интерфейс ограничен, только если он имеет атрибут limited (или synchronized и т.д). Как было упомянуто ранее, потомок неограниченного интерфейса должен быть неограничен, так как это должно осуществить назначение и равенство. Так, если интерфейс составлен из набора ограниченных и неограниченных интерфейсов, он должен быть неограничен.

  1        type I is interface and LI and NLI;         -- legal
  2        type I is limited interface and LI and NLI; -- illegal

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

  1        type LT is limited tagged ...
  2        type NLT is tagged ...

@ тогда:

  1        type T is new NLT and LI  with ... -- legal, T not limited
  2        type T is new NLT and NLI with ... -- legal, T not limited
  3        type T is new LT  and LI  with ... -- legal, T limited
  4        type T is new LT  and NLI with ... -- illegal

@ Последнее незаконно, потому что T, как ожидается, будет ограничен, потому что он наследуется из ограниченного родительского типа LT, и все же он - также потомок неограниченного интерфейса NLI.

@ Чтобы избежать определенных курьёзных трудностей, Ада 2005 требует явной установки атрибута limited при наследовании типа. (Было бы хорошо настаивать на этом всегда для ясности, но такое изменение будет слишком большой несовместимостью.), Если мы действительно заявляем limited явно тогда, родитель должен быть ограничен (не зависимо от того тип это или интерфейс).

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

  1        type T is limited new LI with ...

@ Эти правила сводятся к одной вещи. Если родитель или прародитель (действительно предок) неограничены тогда, потомок должен быть неограничен. И наоборот, если тип (включая интерфейс) ограничен, тогда все его предки должны быть ограничены.

@ Более ранняя версия Ады 2005 столкнулась с трудностями в этой области, потому что в случае типа, наследуемого только из интерфейсов, поведение могло зависеть от порядка их появления в списке (потому что правила для родителя и прародителей немного отличны). Но в заключительной версии языка порядок не имеет значения. Так:

  1        type T is new NLI and LI with ... -- legal, not limited
  2        type T is new LI and NLI with ... -- legal, not limited

@ Но следующее конечно незаконно:

  1        type T is limited new NLI and LI with ... -- illegal
  2        type T is limited new LI and NLI with ... -- illegal

@ Есть также подобные изменения к настраиваемым формальным параметрам и расширениям типов - Ада 2005 требует указывать атрибут limited явно в обоих случаях.

@ ENG RUS

TOP BACK NEXT

2010-10-31 15:16:26

. .