Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






Nbsp;   Components, Polymorphism, and Versioning

Object-oriented programming (OOP) has been around for many, many years. When it was first used in the late 1970s/early 1980s, applications were much smaller in size and all the code to make the application run was written by one company. Sure, there were operating systems back then and ap- plications did make use of what they could out of those operating systems, but the operating systems offered very few features compared with the operating systems of today.

Today, software is much more complex and users demand that applications offer rich features such as GUIs, menu items, mouse input, tablet input, printer output, networking, and so on. For this rea- son, our operating systems and development platforms have grown substantially over recent years.

Furthermore, it is no longer feasible or even cost effective for application developers to write all of the code necessary for their application to work the way users expect. Today, applications consist of code produced by many different companies. This code is stitched together using an object-oriented paradigm.

Component Software Programming (CSP) is OOP brought to this level. Here are some attributes of a component:

■ A component (an assembly in the .NET Framework) has the feeling of being “published.”

 

■ A component has an identity (a name, version, culture, and public key).

 

■ A component forever maintains its identity (the code in an assembly is never statically linked into another assembly; .NET always uses dynamic linking).

■ A component clearly indicates the components it depends upon (reference metadata tables).

 

■ A component should document its classes and members. C# offers this by allowing in-source Extensible Markup Language (XML) documentation along with the compiler’s /doc command- line switch.

■ A component must specify the security permissions it requires. The CLR’s code access security (CAS) facilities enable this.


■ A component publishes an interface (object model) that won’t change for any servicings. A servicing is a new version of a component whose intention is to be backward compatible with the original version of the component. Typically, a servicing version includes bug fixes, security patches, and possibly some small feature enhancements. But a servicing cannot require any new dependencies or any additional security permissions.

As indicated by the last bullet, a big part of CSP has to do with versioning. Components will change over time and components will ship on different time schedules. Versioning introduces a whole new level of complexity for CSP that didn’t exist with OOP, with which all code was written, tested, and shipped as a single unit by a single company. In this section, I’m going to focus on com- ponent versioning.

In the .NET Framework, a version number consists of four parts: a major part, a minor part, a build part, and a revision part. For example, an assembly whose version number is 1.2.3.4 has a major part of 1, a minor part of 2, a build part of 3, and a revision part of 4. The major/minor parts are typically used to represent a consistent and stable feature set for an assembly and the build/revision parts are typically used to represent a servicing of this assembly’s feature set.



Let’s say that a company ships an assembly with version 2.7.0.0. If the company later wants to fix a bug in this component, they would produce a new assembly in which only the build/revision parts of the version are changed, something like version 2.7.1.34. This indicates that the assembly is a servicing whose intention is to be backward compatible with the original component (version 2.7.0.0).

On the other hand, if the company wants to make a new version of the assembly that has signifi- cant changes to it and is therefore not intended to be backward compatible with the original assem- bly, the company is really creating a new component and the new assembly should be given a version number in which the major/minor parts are different from the existing component (version 3.0.0.0, for example).

       
   
 
 

 

Now that we’ve looked at how we use version numbers to update a component’s identity to reflect a new version, let’s take a look at some of the features offered by the CLR and programming languages (such as C#) that allow developers to write code that is resilient to changes that may be occurring in components that they are using.

Versioning issues come into play when a type defined in a component (assembly) is used as the base class for a type in another component (assembly). Obviously, if the base class versions (changes) underneath the derived class, the behavior of the derived class changes as well, probably in a way that causes the class to behave improperly. This is particularly true in polymorphism scenarios in which a derived type overrides virtual methods defined by a base type.


C# offers five keywords that you can apply to types and/or type members that impact component versioning. These keywords map directly to features supported in the CLR to support component ver- sioning. Table 6-2 contains the C# keywords related to component versioning and indicates how each keyword affects a type or type member definition.

 

TABLE 6-2C# Keywords and How They Affect Component Versioning

 

C# Keyword Type Method/Property/Event Constant/Field
abstract Indicates that no instances of the type can be constructed Indicates that the derived type must override and implement this member before instances of the de- rived type can be constructed (not allowed)
virtual (not allowed) Indicates that this member can be over- ridden by a derived type (not allowed)
override (not allowed) Indicates that the derived type is over- riding the base type’s member (not allowed)
sealed Indicates that the type cannot be used as a base type Indicates that the member cannot be overridden by a derived type. This key- word can be applied only to a method that is overriding a virtual method (not allowed)
new When applied to a nested type, meth- od, property, event, constant, or field, indicates that the member has no rela- tionship to a similar member that may exist in the base class    

 

I will demonstrate the value and use of all these keywords in the upcoming section titled “Dealing with Virtual Methods When Versioning Types.” But before we get to a versioning scenario, let’s focus on how the CLR actually calls virtual methods.

 

How the CLR Calls Virtual Methods, Properties, and Events

In this section, I will be focusing on methods, but this discussion is relevant to virtual properties and virtual events as well. Properties and events are actually implemented as methods; this will be shown in their corresponding chapters.

Methods represent code that performs some operation on the type (static methods) or an instance of the type (nonstatic methods). All methods have a name, a signature, and a return type (that may be void). The CLR allows a type to define multiple methods with the same name as long as each method has a different set of parameters or a different return type. So it’s possible to define two methods with the same name and same parameters as long as the methods have a different return type. However, except for IL assembly language, I’m not aware of any language that takes advantage of this “feature”; most languages (including C#) require that methods differ by parameters and ignore a method’s return type when determining uniqueness. (C# actually relaxes this restriction when defin- ing conversion operator methods; see Chapter 8 for details.)


The Employee class shown below defines three different kinds of methods.

 

internal class Employee {

// A nonvirtual instance method

public Int32 GetYearsEmployed() { ... }

 

// A virtual method (virtual implies instance) public virtual String GetProgressReport() { ... }

 

// A static method

public static Employee Lookup(String name) { ... }

}

 

When the compiler compiles this code, the compiler emits three entries in the resulting assembly’s

method definition table. Each entry has flags set indicating if the method is instance, virtual, or static.

 

When code is written to call any of these methods, the compiler emitting the calling code ex- amines the method definition’s flags to determine how to emit the proper IL code so that the call is made correctly. The CLR offers two IL instructions for calling a method:

■ The call IL instruction can be used to call static, instance, and virtual methods. When the call instruction is used to call a static method, you must specify the type that defines the method that the CLR should call. When the call instruction is used to call an instance or virtual method, you must specify a variable that refers to an object. The call instruction assumes that this variable is not null. In other words, the type of the variable itself indicates which type defines the method that the CLR should call. If the variable’s type doesn’t define the method, base types are checked for a matching method. The call instruction is frequently used to call a virtual method nonvirtually.

■ The callvirt IL instruction can be used to call instance and virtual methods, not static meth- ods. When the callvirt instruction is used to call an instance or virtual method, you must specify a variable that refers to an object. When the callvirt IL instruction is used to call a nonvirtual instance method, the type of the variable indicates which type defines the method that the CLR should call. When the callvirt IL instruction is used to call a virtual instance method, the CLR discovers the actual type of the object being used to make the call and then calls the method polymorphically. In order to determine the type, the variable being used to make the call must not be null. In other words, when compiling this call, the JIT compiler generates code that verifies that the variable’s value is not null. If it is null, the callvirt in- struction causes the CLR to throw a NullReferenceException. This additional check means that the callvirt IL instruction executes slightly more slowly than the call instruction.

Note that this null check is performed even when the callvirt instruction is used to call a nonvirtual instance method.


So now, let’s put this together to see how C# uses these different IL instructions.

 

using System;

 

public sealed class Program { public static void Main() {

Console.WriteLine(); // Call a static method

 

Object o = new Object();

o.GetHashCode(); // Call a virtual instance method o.GetType(); // Call a nonvirtual instance method

}

}

 

If you were to compile the code above and look at the resulting IL, you’d see the following.

 

.method public hidebysig static void Main() cil managed {

.entrypoint

// Code size 26 (0x1a)

.maxstack 1

.locals init (object o)

IL_0000: call void System.Console::WriteLine() IL_0005: newobj instance void System.Object::.ctor() IL_000a: stloc.0

IL_000b: ldloc.0

IL_000c: callvirt instance int32 System.Object::GetHashCode() IL_0011: pop

IL_0012: ldloc.0

IL_0013: callvirt instance class System.Type System.Object::GetType() IL_0018: pop

IL_0019: ret

} // end of method Program::Main

 

Notice that the C# compiler uses the call IL instruction to call Console’s WriteLine method.

This is expected because WriteLine is a static method. Next, notice that the callvirt IL instruction is used to call GetHashCode. This is also expected, because GetHashCode is a virtual method. Finally, notice that the C# compiler also uses the callvirt IL instruction to call the GetType method. This is surprising because GetType is not a virtual method. However, this works because while JIT-compiling this code, the CLR will know that GetType is not a virtual method, and so the JIT-compiled code will simply call GetType nonvirtually.

Of course, the question is, why didn’t the C# compiler simply emit the call instruction instead? The answer is because the C# team decided that the JIT compiler should generate code to verify that the object being used to make the call is not null. This means that calls to nonvirtual instance methods are a little slower than they could be. It also means that the following C# code will cause a NullReferenceException to be thrown. In some other programming languages, the intention of the following code would run just fine.

 

using System;

 

public sealed class Program {

public Int32 GetFive() { return 5; } public static void Main() {


Program p = null;

Int32 x = p.GetFive(); // In C#, NullReferenceException is thrown

}

}

 

Theoretically, the preceding code is fine. Sure, the variable p is null, but when calling a nonvirtual method (GetFive), the CLR needs to know just the data type of p, which is Program. If GetFive did get called, the value of the this argument would be null. Because the argument is not used inside the GetFive method, no NullReferenceException would be thrown. However, because the C# compiler emits a callvirt instruction instead of a call instruction, the preceding code will end up throwing the NullReferenceException.

       
   
 
 

 

Sometimes, the compiler will use a call instruction to call a virtual method instead of using a callvirt instruction. At first, this may seem surprising, but the code below demonstrates why it is sometimes required.

 

internal class SomeClass {

// ToString is a virtual method defined in the base class: Object. public override String ToString() {

 

// Compiler uses the 'call' IL instruction to call

// Object’s ToString method nonvirtually.

 

// If the compiler were to use 'callvirt' instead of 'call', this

// method would call itself recursively until the stack overflowed. return base.ToString();

}

}

 

When calling base.ToString (a virtual method), the C# compiler emits a call instruction to ensure that the ToString method in the base type is called nonvirtually. This is required because if ToString were called virtually, the call would execute recursively until the thread’s stack overflowed, which obviously is not desired.

Compilers tend to use the call instruction when calling methods defined by a value type because value types are sealed. This implies that there can be no polymorphism even for their virtual methods, which causes the performance of the call to be faster. In addition, the nature of a value type instance guarantees it can never be null, so a NullReferenceException will never be thrown. Finally, if you


were to call a value type’s virtual method virtually, the CLR would need to have a reference to the value type’s type object in order to refer to the method table within it. This requires boxing the value type. Boxing puts more pressure on the heap, forcing more frequent garbage collections and hurting performance.

Regardless of whether call or callvirt is used to call an instance or virtual method, these methods always receive a hidden this argument as the method’s first parameter. The this argument refers to the object being operated on.

When designing a type, you should try to minimize the number of virtual methods you define. First, calling a virtual method is slower than calling a nonvirtual method. Second, virtual methods cannot be inlined by the JIT compiler, which further hurts performance. Third, virtual methods make versioning of components more brittle, as described in the next section. Fourth, when defining a base type, it is common to offer a set of convenience overloaded methods. If you want these methods to be polymorphic, the best thing to do is to make the most complex method virtual and leave all of the convenience overloaded methods nonvirtual. By the way, following this guideline will also improve the ability to version a component without adversely affecting the derived types. Here is an example.

 

public class Set {

private Int32 m_length = 0;

 

// This convenience overload is not virtual public Int32 Find(Object value) {

return Find(value, 0, m_length);

}

 

// This convenience overload is not virtual

public Int32 Find(Object value, Int32 startIndex) { return Find(value, startIndex, m_length ­ startIndex);

}

 

// The most feature­rich method is virtual and can be overridden

public virtual Int32 Find(Object value, Int32 startIndex, Int32 endIndex) {

// Actual implementation that can be overridden goes here...

}

 

// Other methods go here

}

 

 

Using Type Visibility and Member Accessibility Intelligently

With the .NET Framework, applications are composed of types defined in multiple assemblies pro- duced by various companies. This means that the developer has little control over the components he or she is using and the types defined within those components. The developer typically doesn’t have access to the source code (and probably doesn’t even know what programming language was used to create the component), and components tend to version with different schedules. Furthermore, due to polymorphism and protected members, a base class developer must trust the code written by the derived class developer. And, of course, the developer of a derived class must trust the code that he


is inheriting from a base class. These are just some of the issues that you need to really think about when designing components and types.

In this section, I’d like to say just a few words about how to design a type with these issues in mind.

Specifically, I’m going to focus on the proper way to set type visibility and member accessibility so

that you’ll be most successful.

 

First, when defining a new type, compilers should make the class sealed by default so that the class cannot be used as a base class. Instead, many compilers, including C#, default to unsealed classes and allow the programmer to explicitly mark a class as sealed by using the sealed keyword. Obviously, it is too late now, but I think that today’s compilers have chosen the wrong default and it would be nice if this could change with future compilers. There are three reasons why a sealed class is better than an unsealed class:

VersioningWhen a class is originally sealed, it can change to unsealed in the future without breaking compatibility. However, after a class is unsealed, you can never change it to sealed in the future as this would break all derived classes. In addition, if the unsealed class defines any unsealed virtual methods, ordering of the virtual method calls must be maintained with new versions or there is the potential of breaking derived types in the future.

PerformanceAs discussed in the previous section, calling a virtual method doesn’t perform as well as calling a nonvirtual method because the CLR must look up the type of the object

at run time in order to determine which type defines the method to call. However, if the JIT compiler sees a call to a virtual method using a sealed type, the JIT compiler can produce more efficient code by calling the method nonvirtually. It can do this because it knows there can’t possibly be a derived class if the class is sealed. For example, in the code below, the JIT compiler can call the virtual ToString method nonvirtually.

 

using System;

public sealed class Point { private Int32 m_x, m_y;

 

public Point(Int32 x, Int32 y) { m_x = x; m_y = y; }

 

public override String ToString() {

return String.Format("({0}, {1})", m_x, m_y);

}

 

public static void Main() { Point p = new Point(3, 4);

 

// The C# compiler emits the callvirt instruction here but the

// JIT compiler will optimize this call and produce code that

// calls ToString nonvirtually because p's type is Point,

// which is a sealed class Console.WriteLine(p.ToString());

}

}


Security and predictabilityA class must protect its own state and not allow itself to ever become corrupted. When a class is unsealed, a derived class can access and manipulate the base class’s state if any data fields or methods that internally manipulate fields are accessible and not private. In addition, a virtual method can be overridden by a derived class, and the derived class can decide whether to call the base class’s implementation. By making a method, property, or event virtual, the base class is giving up some control over its behavior and its state. Unless carefully thought out, this can cause the object to behave unpredictably, and it opens up potential security holes.

The problem with a sealed class is that it can be a big inconvenience to users of the type. Occasion- ally, developers want to create a class derived from an existing type in order to attach some additional fields or state information for their application’s own use. In fact, they may even want to define some helper or convenience methods on the derived type to manipulate these additional fields. Although the CLR offers no mechanism to extend an already-built type with helper methods or fields, you can simulate helper methods by using C#’s extension methods (discussed in Chapter 8) and you can simu- late tacking state onto an object by using the ConditionalWeakTable class (discussed in Chapter 21, “The Managed Heap and Garbage Collection.”

Here are the guidelines I follow when I define my own classes:

 

■ When defining a class, I always explicitly make it sealed unless I truly intend for the class to be a base class that allows specialization by derived classes. As stated earlier, this is the op- posite of what C# and many other compilers default to today. I also default to making the class internal unless I want the class to be publicly exposed outside of my assembly. Fortunately, if you do not explicitly indicate a type’s visibility, the C# compiler defaults to internal. If I really feel that it is important to define a class that others can derive but I do not want to allow spe- cialization, I will simulate creating a closed class by using the above technique of sealing the virtual methods that my class inherits.

■ Inside the class, I always define my data fields as private and I never waver on this. Fortu- nately, C# does default to making fields private. I’d actually prefer it if C# mandated that all fields be private and that you could not make fields protected, internal, public, and so on. Exposing state is the easiest way to get into problems, have your object behave unpre- dictably, and open potential security holes. This is true even if you just declare some fields as internal. Even within a single assembly, it is too hard to track all code that references a field, especially if several developers are writing code that gets compiled into the same assembly.

■ Inside the class, I always define my methods, properties, and events as private and nonvir- tual. Fortunately, C# defaults to this as well. Certainly, I’ll make a method, property, or event public to expose some functionality from the type. I try to avoid making any of these mem- bers protected or internal, because this would be exposing my type to some potential vulnerability. However, I would sooner make a member protected or internal than I would make a member virtual because a virtual member gives up a lot of control and really relies on the proper behavior of the derived class.


■ There is an old OOP adage that goes like this: when things get too complicated, make more types. When an implementation of some algorithm starts to get complicated, I define helper types that encapsulate discrete pieces of functionality. If I’m defining these helper types for use by a single über-type, I’ll define the helper types nested within the über-type. This allows for scoping and also allows the code in the nested, helper type to reference the private mem- bers defined in the über-type. However, there is a design guideline rule, enforced by the Code Analysis tool (FxCopCmd.exe) in Visual Studio, which indicates that publicly exposed nested types should be defined at file or assembly scope and not be defined within another type. This rule exists because some developers find the syntax for referencing nested types cumbersome. I appreciate this rule, and I never define public nested types.

 

Dealing with Virtual Methods When Versioning Types

As was stated earlier, in a Component Software Programming environment, versioning is a very important issue. I talked about some of these versioning issues in Chapter 3, “Shared Assemblies and Strongly Named Assemblies,” when I explained strongly named assemblies and discussed how an administrator can ensure that an application binds to the assemblies that it was built and tested with. However, other versioning issues cause source code compatibility problems. For example, you must be very careful when adding or modifying members of a type if that type is used as a base type. Let’s look at some examples.

CompanyA has designed the following type, Phone.

 

namespace CompanyA { public class Phone {

public void Dial() { Console.WriteLine("Phone.Dial");

// Do work to dial the phone here.

}

}

}

 

Now imagine that CompanyB defines another type, BetterPhone, which uses CompanyA’s Phone

type as its base.

 

namespace CompanyB {

public class BetterPhone : CompanyA.Phone { public void Dial() {

Console.WriteLine("BetterPhone.Dial"); EstablishConnection();

base.Dial();

}

 

protected virtual void EstablishConnection() { Console.WriteLine("BetterPhone.EstablishConnection");

// Do work to establish the connection.

}

}

}


When CompanyB attempts to compile its code, the C# compiler issues the following message.

 

warning CS0108: 'CompanyB.BetterPhone.Dial()' hides inherited member 'CompanyA.Phone.Dial()'.

 

Use the new keyword if hiding was intended.” This warning is notifying the developer that BetterPhone is defining a Dial method, which will hide the Dial method defined in Phone. This new method could change the semantic meaning of Dial (as defined by CompanyA when it originally created the Dial method).

It’s a very nice feature of the compiler to warn you of this potential semantic mismatch. The com- piler also tells you how to remove the warning by adding the new keyword before the definition of Dial in the BetterPhone class. Here’s the fixed BetterPhone class.

 

namespace CompanyB {

public class BetterPhone : CompanyA.Phone {

 

// This Dial method has nothing to do with Phone's Dial method. public new void Dial() {

Console.WriteLine("BetterPhone.Dial"); EstablishConnection();

base.Dial();

}

 

protected virtual void EstablishConnection() { Console.WriteLine("BetterPhone.EstablishConnection");

// Do work to establish the connection.

}

}

}

 

At this point, CompanyB can use BetterPhone.Dial in its application. Here’s some sample code that CompanyB might write.

 

public sealed class Program { public static void Main() {

CompanyB.BetterPhone phone = new CompanyB.BetterPhone(); phone.Dial();

}

}

 

When this code runs, the following output is displayed.

 

BetterPhone.Dial BetterPhone.EstablishConnection Phone.Dial

 

This output shows that CompanyB is getting the behavior it desires. The call to Dial is calling the new Dial method defined by BetterPhone, which calls the virtual EstablishConnection method and then calls the Phone base type’s Dial method.

Now let’s imagine that several companies have decided to use CompanyA’s Phone type. Let’s further imagine that these other companies have decided that the ability to establish a connection in the Dial method is a really useful feature. This feedback is given to CompanyA, which now revises its Phone class.


namespace CompanyA { public class Phone {

public void Dial() { Console.WriteLine("Phone.Dial"); EstablishConnection();

// Do work to dial the phone here.

}

 

protected virtual void EstablishConnection() { Console.WriteLine("Phone.EstablishConnection");

// Do work to establish the connection.

}

}

}

 

Now when CompanyB compiles its BetterPhone type (derived from this new version of Com- panyA’s Phone), the compiler issues this message.

 

warning CS0114: 'CompanyB.BetterPhone.EstablishConnection()' hides inherited member 'CompanyA. Phone.EstablishConnection()'. To make the current member override that implementation, add the override keyword. Otherwise, add the new keyword.

 

The compiler is alerting you to the fact that both Phone and BetterPhone offer an Establish­ Connection method and that the semantics of both might not be identical; simply recompiling BetterPhone can no longer give the same behavior as it did when using the first version of the Phone type.

If CompanyB decides that the EstablishConnection methods are not semantically identical in both types, CompanyB can tell the compiler that the Dial and EstablishConnection method defined in BetterPhone is the correct method to use and that it has no relationship with the EstablishConnection method defined in the Phone base type. CompanyB informs the compiler of this by adding the new keyword to the EstablishConnection method.

 

namespace CompanyB {

public class BetterPhone : CompanyA.Phone {

 

// Keep 'new' to mark this method as having no

// relationship to the base type's Dial method. public new void Dial() {

Console.WriteLine("BetterPhone.Dial"); EstablishConnection();

base.Dial();

}

 

// Add 'new' to mark this method as having no

// relationship to the base type's EstablishConnection method. protected new virtual void EstablishConnection() {

Console.WriteLine("BetterPhone.EstablishConnection");

// Do work to establish the connection.

}

}

}


In this code, the new keyword tells the compiler to emit metadata, making it clear to the CLR that

BetterPhone’s EstablishConnection method is intended to be treated as a new function that is introduced by the BetterPhone type. The CLR will know that there is no relationship between Phone’s and BetterPhone’s methods.

When the same application code (in the Main method) executes, the output is as follows.

 

BetterPhone.Dial BetterPhone.EstablishConnection Phone.Dial Phone.EstablishConnection

 

This output shows that Main’s call to Dial calls the new Dial method defined by Better­ Phone.Dial, which in turn calls the virtual EstablishConnection method that is also defined by BetterPhone. When BetterPhone’s EstablishConnection method returns, Phone’s Dial

method is called. Phone’s Dial method calls EstablishConnection, but because BetterPhone’s EstablishConnection is marked with new, BetterPhone’s EstablishConnection method isn’t considered an override of Phone’s virtual EstablishConnection method. As a result, Phone’s Dial method calls Phone’s EstablishConnection method—this is the expected behavior.

       
   
 
 

 

Alternatively, CompanyB could have gotten the new version of CompanyA’s Phone type and decided that Phone’s semantics of Dial and EstablishConnection are exactly what it’s been looking for.

In this case, CompanyB would modify its BetterPhone type by removing its Dial method entirely. In addition, because CompanyB now wants to tell the compiler that BetterPhone’s Establish­

Connection method is related to Phone’s EstablishConnection method, the new keyword must be removed. Simply removing the new keyword isn’t enough, though, because now the compiler can’t tell exactly what the intention is of BetterPhone’s EstablishConnection method. To express his intent exactly, the CompanyB developer must also change BetterPhone’s EstablishConnection method from virtual to override. The following code shows the new version of BetterPhone.


namespace CompanyB {

public class BetterPhone : CompanyA.Phone {

 

// Delete the Dial method (inherit Dial from base).

 

// Remove 'new' and change 'virtual' to 'override' to

// mark this method as having a relationship to the base

// type's EstablishConnection method.

protected override void EstablishConnection() { Console.WriteLine("BetterPhone.EstablishConnection");

// Do work to establish the connection.

}

}

}

 

Now when the same application code (in the Main method) executes, the output is as follows.

 

Phone.Dial BetterPhone.EstablishConnection

 

This output shows that Main’s call to Dial calls the Dial method defined by Phone and inher- ited by BetterPhone. Then when Phone’s Dial method calls the virtual EstablishConnection method, BetterPhone’s EstablishConnection method is called because it overrides the virtual EstablishConnection method defined by Phone.


 


C HA P T E R 7


Date: 2016-03-03; view: 574


<== previous page | next page ==>
Nbsp;   Partial Classes, Structures, and Interfaces | Nbsp;   Constants
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.032 sec.)