Specialization, Extension, and Inheritance |
A complete PDF version of the text book is now available. The PDF version is an almost complete subset of the HTML version (where only a few, long program listings have been removed). See here. |
In Chapter 27 we discussed inheritance in general. In this section we will be more specific about class inheritance in C#. The current section is long, not least because it covers important details about virtual methods and polymorphism.
28.1. Class Inheritance in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
When we define a class, say class-name, we can give the name of the superclass, super-class-name, of the class. The syntax of this is shown in Chapter 27. In some contexts, a superclass is also called a base class.
| |||
|
We see that the superclass name is given after the colon. There is no keyword involved (like extends in Java). If a class implements interfaces, see Chapter 31, the names of these interfaces are also listed after the colon. The superclass name must be given before the names of interfaces. If we do not give a superclass name after the colon, it is equivalent to writing : Object. In other words, a class, which does not specify an explicit superclass, inherits from class Object. We discuss class Object in Section 28.2 and Section 28.3.
In Program 28.1 below we show a class B which inherits from class A. Notice that Program 28.1 uses C# syntax, and that the figure shows full class definitions. Notice also that the set of member is empty in both class A and B. As before, we use the graphical notation in Figure 28.1 for this situation.
1 2 3 | class A {} class B: A {} | |||
|
Figure 28.1 The class B inherits from class A |
B is said to be a subclass of A, and A a superclass of B. A is also called the base class of B. |
28.2. The top of the class hierarchy
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
As discussed in Section 27.4 a set of classes define a class hierarchy. The top/root of the class hierarchy is the class called Object. More precisely, the only class which does not have an edge to a superclass in the class graph is called Object. In C# the class Object resides in the System namespace. The type object is an alias for System.Object. Due to inheritance the methods in class Objects are available in all types in C#, including value types. We enumerate these methods in Section 28.3.
Figure 28.2 The overall type hierarchy in C# |
The left branch of Figure 28.2 corresponds to the reference types of C#. Reference types were discussed in Chapter 13. The right branch of Figure 28.2 corresponds to the value types, which we have discussed in Chapter 14.
All pre-existing library classes, and the classes we define in our own programs, are reference types. We have also emphasized that strings (as represented by class String) and arrays (as represented by class Array) are reference types. Notice that the dotted box "Reference types" is imaginary and non-existing. (We have added it for matters of symmetry, and for improved conceptual overview). The role of class Array is clarified in Section 47.1.
The class ValueType is the base type of all value types. Its subclass Enum is a base type of all enumeration types. It is a little confusing that these two classes are used as superclasses of structs, in particular because structs cannot inherit from other structs or classes. This can be seen as a special-purpose organization, made by the C# language designers. We cannot, as programmers, replicate such organizations in our own programs. The classes Object, ValueType and Enum contain methods, which are available in the more specialized value types (defined by structs) of C#.
28.3. Methods in the class Object in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
We will now review the methods in class Object. Due to the type organization discussed in Section 28.2 these methods can be used uniformly in all classes and in all structs.
|
There are three equality methods in class Object. All three of them have been discussed in Section 13.5. The instance methods Equals is the one we often redefine in case we need a shallow equality operation in one of our classes. See Section 28.16 for details. The static method, also named Equals, is slightly more applicable because it can also compare objects/values and null values. The static method ReferenceEquals is - at least in the starting point - equivalent to the == operator.
The instance method GetHashCode produces an integer value which can be used for indexing purposes in hashtables. In order to obtain efficient implementations, GetHashCode often use some of the bit-wise operators, such as shifting and bit-wise exclusive or. (See Program 28.29 for an example). It must be ensured that if o1.Equals(o2) then o1.GetHashCode() has the same value as o2.GetHashCode().
The instance method ToString is well-known. We have seen it in numerous types, for instance in the very first Die class we wrote in Program 10.1. We implement and override this method in most of our classes. ToString is implicitly called whenever we need some text string representation of an object obj, typically in the context of an output statement such Console.WriteLine("{0}", obj). If the parameterless ToString method of class Object is not sufficient for our formatting purposes, we can implement the ToString method of the interface IFormattable, see Section 31.7.
The protected and virtual method Finalize allows an object to release resources just before garbage collection is carried out. Finalize methods must defined indirectly via destructor syntax. In a class C, the destructor is denoted ~C(){...}. It is not possible, in a direct way, to redefine Finalize. Thus, Finalize is treated in a special way by the compiler.
MemberwiseClone is a protected method which does bit per bit copying of an object (shallow copying, see Section 13.4 ). MemberwiseClone can be used in subclasses of Object (in all classes and structs), but MemberwiseClone cannot be used from clients because it is not public. In Section 32.7 we will see how to make cloning available in the client interface; This involves implementation of the interface ICloneable (see Section 31.4) and delegation to MemberwiseClone from the Clone method prescribed by ICloneable.
28.4. Inheritance and Constructors
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Constructors in C# were introduced in Section 12.4 as a means for initializing objects, cf. Section 12.3. It is recommended to review the basic rules for definition of constructors in Section 12.4.
As the only kind of members, constructors are not inherited. This is because a constructor is only useful in the class to which it belongs. In terms of the BankAccount class hierarchy shown in Figure 25.3, the BankAccount constructor is not directly useful as an inherited member of the class CheckAccount: It would not be natural to apply a BankAccount constructor on a CheckAccount object.
On the other hand, the BankAccount constructor typically does part of the work of a CheckAccount constructor. Therefore it is useful for the CheckAccount constructor to call the BankAccount constructor. This is indeed possible in C#. So the statement that "constructors are not inherited" should be taken with a grain of salt. A superclass constructor can be seen and activated in a subclass constructor.
Here follows the overall guidelines for constructors in class hierarchy:
|
As recommended in Section 12.4 you should always program the necessary constructors in each of your classes. As explained and motivated in Section 12.4 it is not possible in C# to mix a parameterless default constructor and the constructors with parameters that you program yourself. You can, however, program your own parameterless constructor and a number of constructors with parameters.
In the same way as two or more constructors in a given class typically cooperate (delegate work to each other using the special this(...) syntax) the constructors of a class C and the constructors of the base class of C cooperate. If a constructor in class C does not explicitly call base(...) in its superclass, it implicitly calls the parameterless constructor in the superclass. In that case, such a parameterless constructor must exist, and it must be non-private.
We will return to the BankAccount class hierarchy from Section 25.4 and emphasize the constructors in the classes that are involved.
In Program 28.2 we see the root bank account class, BankAccount. It has two constructors, where the second is defined by means of the first. Notice the use of the this(...) notation outside the body of the constructor in line 16.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | using System; public class BankAccount { protected double interestRate; protected string owner; protected decimal balance; public BankAccount(string o, decimal b, double ir) { this.interestRate = ir; this.owner = o; this.balance = b; } public BankAccount(string o, double ir): this(o, 0.0M, ir) { } public virtual decimal Balance { get {return balance;} } public virtual void Withdraw (decimal amount) { balance -= amount; } public virtual void Deposit (decimal amount) { balance += amount; } public virtual void AddInterests() { balance += balance * (Decimal)interestRate; } public override string ToString() { return owner + "'s account holds " + + balance + " kroner"; } } | |||
|
The two constructors of the class CheckAccount, shown in Program 28.3, both delegate part of the initialization work to the first constructor in class BankAccount. Again, this is done via the special notation base(...) outside the body of the constructor. Notice that bodies of both constructors in CheckAccount are empty.
It is interesting to ask why the designers of C# have decided on the special way of delegating work between constructors in C#. Alternatively, one constructor could chose to delegate work to another constructor inside the bodies. The rationale behind the C# design is most probably, that the designers insist on a particular initialization order. This will be discussed in Section 28.5.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | using System; public class CheckAccount: BankAccount { public CheckAccount(string o, double ir): base(o, 0.0M, ir) { } public CheckAccount(string o, decimal b, double ir): base(o, b, ir) { } public override void Withdraw (decimal amount) { balance -= amount; if (amount < balance) interestRate = -0.10; } public override string ToString() { return owner + "'s check account holds " + + balance + " kroner"; } } | |||
|
For reasons of completeness we also show the classes SavingsAccount and LotteryAccount in Program 28.4 (only on web) and Program 28.5 (only on web) respectively. As above, we emphasize the constructors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using System; public class SavingsAccount: BankAccount { public SavingsAccount(string o, double ir): base(o, 0.0M, ir) { } public SavingsAccount(string o, decimal b, double ir): base(o, b, ir) { } public override void Withdraw (decimal amount) { if (amount < balance) balance -= amount; else throw new Exception("Cannot withdraw"); } public override void AddInterests() { balance = balance + balance * (decimal)interestRate - 100.0M; } public override string ToString() { return owner + "'s savings account holds " + + balance + " kroner"; } } | |||
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System; public class LotteryAccount: BankAccount { private static Lottery lottery = Lottery.Instance(20); public LotteryAccount(string o, decimal b): base(o, b, 0.0) { } public override void AddInterests() { int luckyNumber = lottery.DrawLotteryNumber; balance = balance + lottery.AmountWon(luckyNumber); } public override string ToString() { return owner + "'s lottery account holds " + + balance + " kroner"; } } | |||
|
28.5. Constructors and initialization order
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
We speculated about the motives behind the special syntax of constructor delegation in the previous section. A constructor in a subclass must - either implicitly or explicitly - activate a constructor in a superclass. In that way a chain of constructors are executed when an object is initialized. The chain of constructors will be called from the most general to the least general. The following initializations take place when a new C object is made with new C(...):
|
Notice that initializers are executed first, from most specific to most general. Next the constructors are called in the opposite direction.
Let us illustrate this by means of concrete example in Program 28.6, Program 28.7 and Program 28.8 where class C inherits from class B, which in turn inherit from class A.
The slightly artificial class Init, shown in Program 28.9 contains a static "tracing method" which returns a given init value, val. More importantly, for our interests, it tells us about the initialization. In that way we can see the initialization order on the standard output stream. The tiny application class, containing the static Main method, is shown in Program 28.10.
The output in Listing 28.11 reveals - as expected - that all initializers are executed before the constructors. First in class C, next in B, and finally in A. After execution of the initializers the constructors are executed. First the A constructors, then the B constructor, and finally the C constructor.
1 2 3 4 5 6 7 8 9 10 | using System; public class C: B { private int varC1 = Init.InitMe(1, "varC1, initializer in class C"), varC2; public C (){ varC2 = Init.InitMe(4, "VarC2, constructor body C"); } } | |||
|
1 2 3 4 5 6 7 8 9 10 | using System; public class B: A { private int varB1 = Init.InitMe(1, "varB1, initializer in class B"), varB2; public B (){ varB2 = Init.InitMe(4, "VarB2, constructor body B"); } } | |||
|
1 2 3 4 5 6 7 8 9 10 | using System; public class A { private int varA1 = Init.InitMe(1, "varA1, initializer in class A"), varA2; public A (){ varA2 = Init.InitMe(4, "VarA2, constructor body A"); } } | |||
|
1 2 3 4 5 6 7 8 9 10 | using System; public class Init{ public static int InitMe(int val, string who){ Console.WriteLine(who); return val; } } | |||
|
1 2 3 4 5 6 7 8 | using System; class App{ public static void Main(){ C c = new C(); } } | |||
|
1 2 3 4 5 6 | varC1, initializer in class C varB1, initializer in class B varA1, initializer in class A VarA2, constructor body A VarB2, constructor body B VarC2, constructor body C | |||
|
28.6. Visibility modifiers in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Visibility control is a key issue in object-oriented programming. The general discussion about visibility appears in Section 11.3, Section 11.4 and Section 11.5. The C# specific discussion is briefly touched on in Section 11.7. We gave overview of visibility in namespaces and types in Section 11.16. In this lecture we have briefly described the issue in general in Section 27.3.
Basically, we must distinguish between visibility of types in assemblies and visibility of members in types:
|
The issue of inheritance and visibility of private members is addressed in Exercise 7.2.
Internal visibility is related to assemblies, not namespaces. Assemblies are produced by the compiler, and represented as either -.dll or -.exe files. It is possible to have a type which is invisible outside the assembly, into which it is compiled. It is, of course, also possible to have types which are visible outside the assembly. This is the mere purpose of having libraries. Per default - if you do not write any modifier - top-level types are internal in their assembly. The ultimate visibility of members of a class, quite naturally, depends on the visibility of the surrounding type in the assembly.
Members of classes (variables, methods, properties, etc) can also have internal visibility. Protected members are visible in direct and indirect subclasses. You can think of protected members as members visible from classes in the inheritance family. We could call it family visibility. It is - as noticed above - possible to combine internal and protected visibility. The default visibility of members in types is private.
It was a major point in Chapter 11 that data should be private within its class. With the introduction of inheritance we may chose to define data as protected members. Protected data is convenient, at least from a short-term consideration, because superclass data then can be seen from subclasses. But having protected data in class C implies that knowledge of the data representation is spread from class C to all direct and indirect subclasses of C. Thus, a larger part of the program is vulnerable if/when the data representation is changed. (Recall the discussion about representation independence from Section 11.6). Therefore we may decide to keep data private, and to access superclass data via public or protected operations. It is worth a serious consideration is you should allow protected data in the classes of your next programming project.
Related to inheritance we should also notice that a redefined member in a subclass should be at least as visible as the member in the superclass, which it replaces. It is possible to introduce visibility inconsistencies. This has been discussed in great details in Section 11.16.
Exercise 7.2. Private Visibility and inheritance Take a look at the classes shown below:
Answer the following questions before you run the program:
Run the program and confirm your answers. Solution |
Exercise 7.3. Internal Visibility The purpose of this exercise is to get some experience with the visibility modifier called internal. Take a look at the slide to which this exercise belongs. In this exercise, it is recommended to activate the compiler from a command prompt. Make a namespace N with two classes P and I:
Compile the classes in the namespace N to a single assembly, for instance located in the file x.dll. Demonstrate that the class I can be used in class P. Also demonstrate that P.i can be seen and used in class P. After this, program a class A, which attempts to use the classes P and I from x.dll. Arrange that class A is compiled separately, to a file y.dll. Answer the following questions about class A:
Finally, arrange that class A is compiled together with N.P and N.I to a single assembly, say y.dll. Does this alternative organization affect the answers to the questions asked above? Solution |
28.7. Inheritance of methods, properties, and indexers
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
All members apart from constructors are inherited. In particular we notice that operations (methods, properties, and indexers) are inherited.
Methods, properties, and indexers are inherited |
Here follows some basic observations about inheritance of operations:
|
The distinctions between virtual/override and new is detailed in Section 28.9.
The subject of the second item is method combination, which we will discuss in more details in Chapter 29.
Operators are inherited. A redefined operator in a subclass will be an entirely new operator. |
Operators (see Chapter 21) are static. The choice of operator is fully determined at compile time. Operators can be overloaded. There are rules, which constrain the types of formal parameters of operators, see Section 21.4. All this implies that two identically named operators in two classes, one of which inherits from the other, can be distinguished from each other already at compile-time.
28.8. Inheritance of methods: Example.
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
We will now carefully explore a concrete example that involves class inheritance. We stick to the bank account classes, as introduced in Section 25.4 where we discussed class specialization. In Program 28.12, Program 28.13, and Program 28.14 we emphasize the relevant aspects of inheritance with colors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | using System; public class BankAccount { protected double interestRate; protected string owner; protected decimal balance; public BankAccount(string o, decimal b, double ir) { this.interestRate = ir; this.owner = o; this.balance = b; } public BankAccount(string o, double ir): this(o, 0.0M, ir) { } public virtual decimal Balance { get {return balance;} } public virtual void Withdraw (decimal amount) { balance -= amount; } public virtual void Deposit (decimal amount) { balance += amount; } public virtual void AddInterests() { balance += balance * (Decimal)interestRate; } public override string ToString() { return owner + "'s account holds " + + balance + " kroner"; } } | |||
|
In Program 28.12 the data a protected, not private. This is an easy solution, but not necessarily the best solution, because the program area that uses the three instance variables of class BankAccount now becomes much larger. This has already been discussed in Section 28.6. In addition the properties and methods are declared as virtual. As we will see in Section 28.14 this implies that we can redefine the operations in subclasses of BankAccount, such that the run-time types of bank accounts (the dynamic types) determine the actual operations carried out.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using System; public class CheckAccount: BankAccount { // Instance variables of BankAccount are inherited public CheckAccount(string o, double ir): base(o, 0.0M, ir) { } public CheckAccount(string o, decimal b, double ir): base(o, b, ir) { } // Method Balance is inherited // Method Deposit is inherited // Method AddInterests is inherited public override void Withdraw (decimal amount) { base.Withdraw(amount); if (amount < balance) interestRate = -0.10; } public override string ToString() { return owner + "'s check account holds " + + balance + " kroner"; } } | |||
|
In class CheckAccount in Program 28.13 the instance variables of class BankAccount and the operations Balance, Deposit, and AddInterests are inherited. Thus, these operations from BankAccount can simply be (re)used on CheckAccount objects. The method Withdraw is redefined. Notice that Withdraw calls base.Withdraw, the Withdraw method in class BankAccount. This is (imperative) method combination, see Section 29.1. As we will see in Section 28.9 the modifier override is crucial. The method ToString overrides the similar method in BankAccount, which in turn override the similar method from class Object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | using System; public class SavingsAccount: BankAccount { // Instance variables of BankAccount are inherited public SavingsAccount(string o, double ir): base(o, 0.0M, ir) { } public SavingsAccount(string o, decimal b, double ir): base(o, b, ir) { } // Method Balance is inherited // Method Deposit is inherited public override void Withdraw (decimal amount) { if (amount < balance) base.Withdraw(amount); else throw new Exception("Cannot withdraw"); } public override void AddInterests() { balance = balance + balance * (decimal)interestRate - 100.0M; } public override string ToString() { return owner + "'s check account holds " + + balance + " kroner"; } } | |||
|
Similar observations apply for class SavingsAccount in Program 28.14 (only on web) and class LotteryAccount Program 28.15 (only on web).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | using System; public class LotteryAccount: BankAccount { // Instance variables of BankAccount are inherited protected static Lottery lottery = Lottery.Instance(3); public LotteryAccount(string o, decimal b): base(o, b, 0.0) { } // Method Balance is inherited // Method Deposit is inherited // Method Withdraw is inherited public override void AddInterests() { int luckyNumber = lottery.DrawLotteryNumber; balance = balance + lottery.AmountWon(luckyNumber); } public override string ToString() { return owner + "'s lottery account holds " + + balance + " kroner"; } } | |||
|
Exercise 7.4. A subclass of LotteryAccount On the slide, to which this exercise belongs, we have emphasized inheritance of methods and properties in the bank account class hierarchy. From the web-version of the material there is direct access to the necessary pieces of program. The LotteryAccount uses an instance of a Lottery object for adding interests. Under some lucky circumstances, the owner of a LotteryAccount will get a substantial amount of interests. In most cases, however, no interests will be added. There exists a single file which contains the classes BankAccount, CheckAccount, SavingsAccount, Lottery, together with a sample client class. Program a specialization of the LotteryAccount, called LotteyPlusAccount, with the following redefinitions of Deposit and Withdraw.
Notice that the Deposit and Withdraw methods in LotteryPlusAccount should combine with the method in LotteryAccount (method combination). Thus, use the Deposit and Withdraw methods from LotteryAccount as much as possible when you program the LotteryPlusAccount. Test-drive the class LotteryPlusAccount from a sample client class. Solution |
28.9. Overriding and Hiding in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Let us now carefully explore the situation where a method M appears in both class A and its subclass B. Thus, the situation is as outlined in Program 28.16.
1 2 3 4 5 6 7 | class A { public void M(){} } class B: A{ public void M(){} } | |||
|
Let us already now reveal that Program 28.16 is illegal in C#. The compiler will complain (with a warning). We will need to add some modifiers in front of the method definitions.
There are basically two different situations that make sense:
|
Intended redefinition is - by far - the most typical situation. We prepare for intended redefinition by declaring the method as virtual in the most general superclass. This causes the method to be virtual in all subclasses. Each subclass that redefines the method must override it. This pattern paves the road for dynamic binding, see Section 28.10. Intended redefinition appears frequently in almost all object-oriented programs. We have already seen it several times in the bank account classes in Program 28.12 - Program 28.15.
Accidental redefinition is much more rare. Instead of declaring M.B as new it is better to give M in B another name. The new modifier should only be used in situations where renaming is not possible nor desirable.
28.10. Polymorphism. Static and dynamic types
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In this section we define the concepts of polymorphism and dynamic binding. In order to be precise about dynamic binding we also define the meaning of static and dynamic types of variables and parameters.
|
'Poly' means 'many' and 'morph' means 'form'. Thus, polymorphism is related to the idea of 'having many forms' or 'having many types'. In the literature, polymorphism is often associated with procedures or functions that can accept parameters of several types. This is called parametric polymorphism. More basically (and as advocated by, for instance, Bertrand Meyer [Meyer88] ), polymorphism can be related to variables. A polymorphic variable or parameter can (at run-time) take values of more than one type. This is called data polymorphism.
A concrete and detailed discussion of dynamic and static types, based on an example, is found in Section 28.11, which is the next section of this material.
Use of the modifiers virtual and override, as discussed in Section 28.9 is synonymous with dynamic binding. We have much more to say about dynamic binding later in this material, more specifically in Section 28.14 and Section 28.15. Polymorphism and good use of dynamic binding is one of the "OOP crown jewels" in relation to inheritance. It means that you should attempt to design your programs such that they take advantage of polymorphism and dynamic binding. For a practical illustration, please compare Program 28.26 and Program 28.27 in Section 28.15.
28.11. Static and dynamic types in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Before we can continue our discussion of virtual methods (dynamic binding) we will give examples of static and dynamic types of variables.
We now apply the definitions from Section 28.10 to the scene in Program 28.17 shown below. As it appears, the class B inherits from class A. In the client of A and B the variable x is declared of type A, and the variable y is declared of type B. In other words, the static type of x is A and the static type of y is B.
Next, in line 10 and 11, we instantiate class A and B. Thus, at the position of line 12, the variable x refers to an object of type A, and the variable y refers to an object of type B. Therefore, at the position of line 12, the dynamic type of x is A and the dynamic type of y is B.
The assignment x = y in line 13 implies that x (as well as y) now refer to a B object. This is possible due polymorphism. Recall that a B object is an A object. You can read about the is-a relation in Section 25.2.
Line 15 causes a compile-time error. The variable y, of static type B, cannot refer an object of type A. An instance of class A is not a B object.
Finally, in line 17, we assign x to y. Recall, that just before line 17 x and y refer to the same B object. Thus, the assignment y = x is harmless in the given situation. Nevertheless, it is illegal! From a general and conservative point of view, the danger is that the variable y of static type B can be assigned to refer to an object of type A. This would be illegal, because an A object is (still) not a B object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class A {} class B: A{} class Client{ public static void Main (){ // Static type Dynamic type A x; // A - B y; // B - x = new A(); // A A TRIVIAL y = new B(); // B B TRIVIAL x = y; // A B OK - TYPICAL y = new A(); // B A Compile time ERROR // Cannot implicitly convert type 'A' to 'B'. y = x; // B B Compile time ERROR ! // Cannot implicitly convert type 'A' to 'B'. } } | |||
|
We will now, in Program 28.18 remedy one of the problems that we encountered above in Program 28.17. In line 16 the assignment y = x succeed if we cast the object, referred to by x, to a B-object. You should think of the cast as a way to assure the compiler that x, at the given point in time, actually refers to a B-object.
In line 15 we attempt a similar cast of the object returned by the expression new A(). (This is an attempted downcast, see Section 28.17). As indicated, this causes a run-time error. It is not possible to convert an A object to a B object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class A {} class B: A{} class Client{ public static void Main (){ // Static type Dynamic type A x; // A - B y; // B - x = new A(); // A A TRIVIAL y = new B(); // B B TRIVIAL x = y; // A B OK - TYPICAL y = (B)new A(); // B A RUNTIME ERROR y = (B)x; // B B NOW OK } } | |||
|
With a good understanding of static and dynamic types of variables you can jump directly to Section 28.14. If you read linearly you will in Section 28.12 and in Section 28.13 encounter the means of expressions in C# for doing type testing and type conversion.
28.12. Type test and type conversion in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
It is possible to test if the dynamic type of a variable v is of type C, and there are two ways to convert (cast) one class type to another
The following gives an overview of the possibilities.
|
As it appears from level 9 of Table 6.1 is an operator in C#. - The explanation of the is operator above is not fully accurate. The expression in the item above is true if v successfully can be converted to the type C by a reference conversion, a boxing conversion, or an unboxing conversion.
It is - every now and then - useful to test the dynamic type of a variable (or expression) by use of the is operator. Notice however, that in many contexts it is unnecessary to do so explicitly. Use of a virtual method (dynamic binding) encompasses an implicit test of the dynamic type of an expression. Such a test is therefore an implicit branching point in a program. In other words, passing a message to an object selects an appropriate method on basis of the type of the receiver object. You should always consider twice if it is really necessary to discriminate with use of the is operator. If your program contains a lot of instance tests (using the is operator) you may not have understood the idea of virtual methods!
The following to forms of type conversion (casting) is supported in C#:
|
The first, (C)v, is know as casting. If C is a class, casting is a way to adjust the static type of a variable or expression. The latter alternative, v as C, is equivalent to (C)v provided that no exceptions are thrown. If (C)v throws an exception, the expression v as C returns null.
Above we have assumed that C is a reference type (a class for instance). It also makes sense to use (T)v where T is value type (such as a struct). In this case a value of the type is converted to another type. We have touched on explicitly programmed type conversions in Section 21.2. See an example in Program 21.3. Casting of a value of value type may change the actual bits behind the value. The casting of a reference, as discussed above, does not change the "bits behind the reference".
as is an operator in the same way as is, see level 9 of Table 6.1. Notice also, at level 13 of the table, that casting is an operator in C#.
The typeof operator can be applied on a typename to obtain the corresponding object of class Type The Object.GetType instance method returns an object of class Type that represents the run-time type of the receiver. |
Examples of casting, and examples of the as and is operators, are given next in Section 28.13.
28.13. Examples of type test and type conversion
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In this section we will see concrete examples of the is, as, and typecasting operators.
In Program 28.19 (only on web) we see a program with three variables ba1, ba2, and ca. The variable ba1 has static and dynamic type BankAccount. Similarly, ca has static and dynamic type CheckAccount. The variable ba2 has static type BankAccount and dynamic type CheckAccount. (This if fine, because a CheckAccount is a BankAccount). As usual, the class CheckAccount is a subclass of BankAccount. You find these classes in Program 25.1 and Program 25.2.
In the statement part of the Main method we test, systematically, the dynamic type of ba1, ba2, and ca against the two types BankAccount and CheckAccount. Each of the resulting six cases is illustrated with an if-else statement. The executed branch is shown in green. The output of Program 28.19 is also shown explicitly in Listing 28.20.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | using System; class App { public static void Main(){ BankAccount ba1 = new BankAccount("George", 1000.0M, 0.01), ba2 = new CheckAccount("Bill", 2000.0M, 0.01); CheckAccount ca = new CheckAccount("John", 2000.0M, 0.01); if (ba1 is BankAccount) Console.WriteLine("ba1 is BankAccount"); else Console.WriteLine("ba1 is NOT BankAccount"); if (ba1 is CheckAccount) Console.WriteLine("ba1 is CheckAccount"); else Console.WriteLine("ba1 is NOT CheckAccount"); if (ba2 is BankAccount) Console.WriteLine("ba2 is BankAccount"); else Console.WriteLine("ba2 is NOT BankAccount"); if (ba2 is CheckAccount) Console.WriteLine("ba2 is CheckAccount"); else Console.WriteLine("ba2 is NOT CheckAccount"); if (ca is BankAccount) Console.WriteLine("ca is BankAccount"); else Console.WriteLine("ca is NOT BankAccount"); if (ca is CheckAccount) Console.WriteLine("ca is CheckAccount"); else Console.WriteLine("ca is NOT CheckAccount"); } } | |||
|
1 2 3 4 5 6 | ba1 is BankAccount ba1 is NOT CheckAccount ba2 is BankAccount ba2 is CheckAccount ca is BankAccount ca is CheckAccount | |||
|
In Program 28.21 we illustrate, systematically, how to use type castings in the bank account class hierarchy. (The BankAccount and CheckAccount classes are located in Program 25.2 and Program 25.2). For general information about type casting consult the previous section, Section 28.12. Like in Program 28.19 we declare the variables ba1, ba2 and ca. Like in Program 28.19 the static types of ba1 and ba2 are BankAccount, and the static type of ca is CheckAccount. The dynamic type of ba1 is a BankAccount; The dynamic type of ba2 is CheckAccount; The dynamic type of ca is CheckAccount. Thus the static and dynamic setup is identical to the scene in Program 28.19. ( ba1, ba2, and ca are the important variables of the examples. The other variables are not really important for the points we want to make).
The green castings in Program 28.21 make most sense. The red casting is illegal. The blue casting are OK, but they do not actually affect the static types of the involved variables.
Let us address the details in Program 28.21. We systematically attempt all possible casting of ba1, ba2, and ca to the types BankAccount and CheckAccount. The casting of ba1 to BankAccount does not change anything; The static type of ba1 is already BankAccount. The casting of ba2 to CheckAccount is illegal, because ba2 references a BankAccount object, not a CheckAccount object. The casting of ba2 to BankAccount does not change anything; The static type of ba2 is already BankAccount, and the dynamic type of ba2 is not affected. The casting of ba2 to CheckAccount in line 19 is legal and substantial. Within the scope of the casting, (CheckAccount)ba2 changes the static type from BankAccount to CheckAccount. The casting in line 19 is an example of a downcasting, see Section 28.17 . The casting in line 21 is an upcasting. Within the scope of the expressions (BankAccount)ca it changes the static type of ca to BankAccount. The final casting of ca to CheckAccount does not change anything.
Let us emphasize that a casting, like in the expression ba2(CheckAccount), does not permanently change the static type of the variable ba2. The casting only affects the given expression.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | using System; class App { public static void Main(){ BankAccount ba1 = new BankAccount("George", 1000.0M, 0.01), ba2 = new CheckAccount("Bill", 2000.0M, 0.01), baRes1, baRes2, baRes3, baRes4, baRes5, baRes6; CheckAccount ca = new CheckAccount("John", 2000.0M, 0.01); baRes1 = (BankAccount)ba1; // OK. But b1 is already of static // type BankAccount baRes2 = (CheckAccount)ba1; // Illegal downcasting. Run-time error baRes3 = (BankAccount)ba2; // OK. But ba2 is already of static // type BankAccount baRes4 = (CheckAccount)ba2; // OK because ba2 refers to a CheckAccount baRes5 = (BankAccount)ca; // OK. Legal upcasting. baRes6 = (CheckAccount)ca; // OK. But ca is already of static // type CheckAccount } } | |||
|
Program 28.22 (only on web) is equivalent to Program 28.21 But instead of using (T)x Program 28.22 uses x as T.
Notice that, at run-time, Program 28.22 survives the illegal casting in Program 28.21.
The output of Program 28.22 is shown in Listing 28.23 (only on web). You are maybe be a little surprised of the output. If so, please use some efforts on Exercise 7.5.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | using System; class App { public static void Main(){ BankAccount ba1 = new BankAccount("George", 1000.0M, 0.01), ba2 = new CheckAccount("Bill", 2000.0M, 0.01), baRes1, baRes2, baRes3, baRes4, baRes5, baRes6; CheckAccount ca = new CheckAccount("John", 2000.0M, 0.01); baRes1 = ba1 as BankAccount; Report(baRes1); baRes2 = ba1 as CheckAccount; Report(baRes2); // null is reported baRes3 = ba2 as BankAccount; Report(baRes3); baRes4 = ba2 as CheckAccount; Report(baRes4); baRes5 = ca as BankAccount; Report(baRes5); baRes6 = ca as CheckAccount; Report(baRes6); } public static void Report(BankAccount ba){ if (ba != null) Console.WriteLine("{0}", ba); else Console.WriteLine("null"); } } | |||
|
1 2 3 4 5 6 | George's account holds 1000,0 kroner null Bill's check account holds 2000,0 kroner Bill's check account holds 2000,0 kroner John's check account holds 2000,0 kroner John's check account holds 2000,0 kroner | |||
|
Exercise 7.5. Static and dynamic types Type conversion with v as T was illustrated with a program on the accompanying slide. The output of the program was confusing and misleading. We want to report the static types of the expressions ba1 as BankAccount, ba1 as CheckAccount, etc. If you access this exercise from the web-version there will be direct links to the appropriate pieces of program. Explain the output of the program. You can examine the classes BankAccount, CheckAccount, SavingsAccount and LotteryAccount, if you need it. Modify the program such that the static type of the expressions bai as BanktypeAccount is reported. Instead of baRes1 = ba1 as BankAccount; Report(baRes1); you should activate some method on the expression ba1 as BankAccount which reveals its static type. In order to do so, it is allowed to add extra methods to the bank account classes. Solution |
28.14. Virtual methods in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
This section continues our discussion of dynamic binding and virtual methods from Section 28.10. We will make good use of the notion of static type and dynamic type, as introduced in Section 28.11.
First of all notice that virtual methods that are overridden in subclasses rely on dynamic binding, as defined in Section 28.10. Also notice that everything we tell about virtual methods also holds for virtual properties and virtual indexers.
The ABC example in Program 28.24 shows two classes, A and B, together with a Client class. B is a subclass of A. The class A holds the methods M, N, O, and P which are redefined somehow in the subclass B.
The compiler issues a warning in line 11 because we have a method M in both class A and class B. Similarly, a warning is issued in line 13 because we have a method O in class B as well as a virtual method O in class A. The warnings tells you that you should either use the modifier override or new when you redefine methods in class B.
M in class B is said to hide M in class A. Similarly, O in class B hides O in class A.
The overriding of N in line 12 (in class B) of the virtual method N in line 5 (from class A) is very typical. Below, in the client program, we explain the consequences of this setup. Please notice this pattern. Object-oriented programmers use it again and again. It is so common that it is the default setup in Java!
The method P in line 14 of class B is declared as new. P in class B hides P in class A. The use of new suppresses the warnings we get for method M and for method O. The use of new has nothing to do with class instantiation. Declaring P as new in B states an accidental name clash between methods in the class hierarchy. P in A and P in B can co-exist, but they are not intended to be related in the same way as N in A and N in B.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using System; class A { public void M( ){Console.WriteLine("M in A");} public virtual void N( ){Console.WriteLine("N in A");} public virtual void O( ){Console.WriteLine("O in A");} public void P( ){Console.WriteLine("P in A");} } class B: A{ public void M( ){Console.WriteLine("M in B");} // warning public override void N( ){Console.WriteLine("N in B");} public void O( ){Console.WriteLine("O in B");} // warning public new void P( ){Console.WriteLine("P in B");} } class Client { public static void Main(){ A aa = new A( ), // aa has static type A, and dynamic type A ab = new B( ); // ab has static type A, and dynamic type B B b = new B( ); // b has static type B, and dynamic type B aa.N( ); ab.N( ); b.N( ); // The dynamic type controls Console.WriteLine( ); aa.P( ); ab.P( ); b.P( ); // The static type controls } } | |||
|
The Client class in Program 28.24 brings objects of class A and B in play. The variable aa refers an A object. The variable ab refers a B object. And finally, the variable b refers a B object as well.
The most noteworthy cases are emphasized in blue. When we call a virtual method N, the dynamic type of the receiving object controls which method to call. Thus in line 23, aa.N() calls the N method in class A, and ab.N() calls the N method in class B. In both cases we dispatch on an object referred from variables of static type A. The dynamic type of the variable controls the dispatching.
In line 25, the expression aa.P() calls the P method in class A, and (most important in this example) ab.P() also class the P method in class A. In both cases the static type of the variables aa and ab control the dispatching. Please consult the program output in Listing 28.25 to confirm these results.
1 2 3 4 5 6 7 | N in A N in B N in B P in A P in A P in B | |||
|
Virtual methods use dynamic binding Properties and indexers can be virtual in the same way as methods |
Let us finally draw the attention to the case where a virtual method M is overridden along a long chain of classes, say A, B, C, D, E, F, G, and H that inherit from each other (B inherits from A, C from B, etc). In the middle of this chain, let us say in class E, the method M is defined as new virtual instead of being overridden. This changes almost everything! It is easy to miss the new virtual method among all the overridden methods. If a variable v of static type A, B, C, or D refers to an object of type H, then v.M() refers to M in D (the level just below the new virtual method). If v is of static type E, F, or G then v.M() refers to M in class H.
28.15. Practical use of virtual methods in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Having survived the ABC example from the previous section, we will now look at a real-life example of virtual methods. We will program a client class of different types of bank account classes, and we will see how the AddInterests method benefits from being virtual.
The bank account classes, used below, were introduced in Section 25.4 in the context of our discussion of specialization. Please take a look at the way the AddInterests methods are defined in Program 25.1, Program 25.3, and Program 25.4. The class CheckAccount inherits the AddInterests method of class BankAccount. SavingsAccount and LotteryAccount override AddInterests.
Notice that the definition of the AddInterests methods follow the pattern of the methods named N in Program 28.24.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using System; public class AccountClient{ public static void Main(){ BankAccount[] accounts = new BankAccount[5]{ new CheckAccount("Per",1000.0M, 0.03), new SavingsAccount("Poul",1000.0M, 0.03), new CheckAccount("Kurt",1000.0M, 0.03), new LotteryAccount("Bent",1000.0M), new LotteryAccount("Lone",1000.0M) }; foreach(BankAccount ba in accounts){ ba.AddInterests(); } foreach(BankAccount ba in accounts){ Console.WriteLine("{0}", ba); } } } | |||
|
The Main method of the AccountClient class in Program 28.27 declares an array of type BankAccount, see line 6. Due to polymorphism (see Section 28.10) it is possible to initialize the array with different types of BankAccount objects, see line 7-13.
We add interests to all accounts in the array in line 15-17. This is done in a foreach loop. The expression ba.AddInterests() calls the most specialized interest adding method in the BankAccount class hierarchy on ba. The dynamic type of ba determines which AddInterests method to call. If, for instance, ba refers to a LotteryAccount, the AddInterests method of class LotteryAccount is used. Please notice that this is indeed the expected result:
The type of the receiver object obj controls the interpretation of messages to obj.
And further, the most specialized method relative to the type of the receiver is called.
Let us - for a moment - assume that we do not have access to virtual methods and dynamic binding. In Program 28.27 we have rewritten Program 28.26 in such a way that we explicitly control the type dispatching. This is the part of Program 28.27 emphasized in purple. Thus, the purple parts of Program 28.26 and Program 28.27 are equivalent. Which version do you prefer? Imagine that many more bank account types were involved, and find out how valuable virtual methods can be for your future programs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | using System; public class AccountClient{ public static void Main(){ BankAccount[] accounts = new BankAccount[5]{ new CheckAccount("Per",1000.0M, 0.03), new SavingsAccount("Poul",1000.0M, 0.03), new CheckAccount("Kurt",1000.0M, 0.03), new LotteryAccount("Bent",1000.0M), new LotteryAccount("Lone",1000.0M) }; foreach(BankAccount ba in accounts){ if (ba is CheckAccount) ((CheckAccount)ba).AddInterests(); else if (ba is SavingsAccount) ((SavingsAccount)ba).AddInterests(); else if (ba is LotteryAccount) ((LotteryAccount)ba).AddInterests(); else if (ba is BankAccount) ((BankAccount)ba).AddInterests(); } foreach(BankAccount ba in accounts){ Console.WriteLine("{0}", ba); } } } | |||
|
Notice that for the purpose of Program 28.27 we have modified the bank account classes such that AddInterests is not virtual any more. Notice also, in line 22, that the last check of ba is against BankAccount. The check against BankAccount must be the last branch of the if-else chain because all the bank accounts b in the example satisfy the predicate b is BankAccount.
The outputs of Program 28.26 and Program 28.27 are identical, and they are shown in Listing 28.28. As it turns out, we were not lucky enough to get interests out of our lottery accounts.
1 2 3 4 5 | Per's check account holds 1030,000 kroner Poul's savings account holds 930,000 kroner Kurt's check account holds 1030,000 kroner Bent's lottery account holds 1000,0 kroner Lone's lottery account holds 1000,0 kroner | |||
|
The use of virtual methods - and dynamic binding - covers a lot of type dispatching
which in naive programs are expressed with if-else chains |
28.16. Overriding the Equals method in a class
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
The Equals instance method in class Object is a virtual method, see Section 28.3. The Equals method is intended to be redefined (overridden) in subclasses of class Object. The circumstances for redefining Equals have been discussed in Focus box 13.1.
It is tricky to do a correct overriding of the virtual Equals method in class Object |
Below we summarize the issues involved when redefining Equals in one of our own classes.
|
We illustrate the rules in Program 28.29, where we override the Equals method in class BankAccount.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | using System; using System.Collections; public class BankAccount { private double interestRate; private string owner; private decimal balance; private long accountNumber; private static long nextAccountNumber = 0; private static ArrayList accounts = new ArrayList(); public BankAccount(string owner): this(owner, 0.0) { } public BankAccount(string owner, double interestRate) { nextAccountNumber++; accounts.Add(this); this.accountNumber = nextAccountNumber; this.interestRate = interestRate; this.owner = owner; this.balance = 0.0M; } public override bool Equals(Object obj){ if (obj == null) return false; else if (this.GetType() != obj.GetType()) return false; else if (ReferenceEquals(this, obj)) return true; else if (this.accountNumber == ((BankAccount)obj).accountNumber) return true; else return false; } public override int GetHashCode(){ return (int)accountNumber ^ (int)(accountNumber >> 32); // XOR of low orders and high orders bits of accountNumber // (of type long) according to GetHashCode API recommendation. } public decimal Balance () { return balance; } public static long NumberOfAccounts (){ return nextAccountNumber; } public static BankAccount GetAccount (long accountNumber){ foreach(BankAccount ba in accounts) if (ba.accountNumber == accountNumber) return ba; return null; } public void Withdraw (decimal amount) { balance -= amount; } public void Deposit (decimal amount) { balance += amount; } public void AddInterests() { balance = balance + balance * (decimal)interestRate; } public override string ToString() { return owner + "'s account, no. " + accountNumber + " holds " + + balance + " kroner"; } } | |||
|
Please follow the pattern in Program 28.29 when you have to redefine Equals in your future classes.
28.17. Upcasting and downcasting in C#
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
Upcasting and downcasting are common words in the literature about object-oriented programming. We have already used these words earlier in this material, see for instance Program 28.21.
Upcasting converts an object of a specialized type to a more general type Downcasting converts an object from a general type to a more specialized type |
Figure 28.3 A specialization hierarchy of bank accounts |
Relative to Figure 28.3 we declare two BankAccount and two LotteryAccount variables in Program 28.30. After line 4 ba2 refers to a BankAccount object, and la2 refers to a LotteryAccount object.
The assignment in line 6 reflects an upcasting. ba1 is allowed to refer to a LotteryAccount, because - conceptually - a LotteryAccount is a BankAccount.
In line 7, we attempt to assign ba2 to la1. This is an attempted downcasting. This is statically invalid, and the compiler will always complain. Notice that in some cases the assignment la1 = ba2 is legal, namely when ba2 refers to a LotteryAccount object. In order to make the compiler happy, you should write la1 = (LotteryAccount)ba2.
In line 9 we attempt to do the downcasting discussed above, but it fails at run-time. The reason is - of course - that ba2 refers to a BankAccount object, and not to a LotteryAccount object.
After having executed line 6, ba1 refers to a LotteryAccount object. Thus, in line 11 we can assign la1 to the reference in ba1. Again, this is a downcasting. As noticed above, the downcasting is necessary to calm the compiler.
1 2 3 4 5 6 7 8 9 10 11 12 | BankAccount ba1, ba2 = new BankAccount("John", 250.0M, 0.01); LotteryAccount la1, la2 = new LotteryAccount("Bent", 100.0M); ba1 = la2; // upcasting - OK // la1 = ba2; // downcasting - Illegal // discovered at compile time // la1 = (LotteryAccount)ba2; // downcasting - Illegal // discovered at run time la1 = (LotteryAccount)ba1; // downcasting - OK // ba1 already refers to a LotteryAccount | |||
|
Upcasting and downcasting reflect different views on a given object The object is not 'physically changed' due to upcasting or downcasting |
The general rules of upcasting and downcasting in class hierarchies in C# can be expresses as follows:
|
28.18. Inheritance and Variables
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
We have focused a lot on methods in the previous sections. We will now summarize how variables are inherited.
Variables (fields) are inherited Variables cannot be virtual |
Variables are inherited. Thus a variable v in a superclass A is present in a subclass B. This is even the case if v is private in class A, see Exercise 7.2.
What happens if a variable v is present in both a superclass and a subclass? A variable can be redefined in the following sense:
|
We illustrate this situation in the ABC example of Program 28.31. Both class A and B have an int variable v. This can be called accidental redefinition, and this is handled in the program by marking v in class B with the modifier new.
Now, in the client class App, we make some A and B objects. In line 17-23 we see that the static type of a variable determines which version of v is accessed. Notice in particular the expression anotherA.v. If variable access had been virtual, anotherA.v would return the value 5. Now we need to adjust the static type explicitly with a type cast (see Section 28.12) to obtain a reference to B.v. This is illustrated in line 21.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; public class A{ public int v = 1; } public class B: A{ public new int v = 5; } public class App{ public static void Main(){ // Static type Dynamic type A anA = new A(), // A A anotherA = new B(); // A B B aB = new B(); // B B Console.WriteLine( "{0}", anA.v // 1 + anotherA.v // 1 + ((B)anotherA).v // 5 + aB.v // 5 ); } } | |||
|
We do not normally use public instance variables! |
The idea of private instance variables and representation independence was discussed in Section 11.6.
28.19. References
[Meyer88] | Bertrand Meyer, Object-oriented software construction. Prentice Hall, 1988. |