CHAPTER 6

image

Member Accessibility and Overloading

One of the important decisions to make when designing an object is how accessible to make the members. In C#, accessibility can be controlled in several ways.

Class Accessibility

The coarsest level at which accessibility (also known as visibility) can be controlled is at the class. In most cases, the only valid modifiers on a class are public, which means that everybody can see the class, and internal. The exception to this is nesting classes inside of other classes, which is a bit more complicated and is covered in Chapter 7.

Internal is a way of granting access to a wider set of classes without granting access to everybody, and it is most often used when writing helper classes that should be hidden from the ultimate user of the class. In the .NET Runtime world, internal equates to allowing access to all classes that are in the same assembly as this class.

image Note  In the C++ world, such accessibility is usually granted by the use of friends, which provides access to a specific class. The use of friends provides greater granularity in specifying who can access a class, but in practice the access provided by internal is sufficient. In general, all classes should be internal unless other assemblies need to be able to access them.

Using Internal on Members

The internal modifier can also be used on a member, which then allows that member to be accessible from classes in the same assembly as itself, but not from classes outside the assembly.

This is especially useful when several public classes need to cooperate, but some of the shared members shouldn’t be exposed to the general public. Consider the following example:

public class DrawingObjectGroup
{
    public DrawingObjectGroup()
    {
       m_objects = new DrawingObject[10];
       m_objectCount = 0;
    }
    public void AddObject(DrawingObject obj)
    {
       if (m_objectCount < 10)
       {
            m_objects[m_objectCount] = obj;
            m_objectCount++;
       }
    }
    public void Render()
    {
       for (int i = 0; i < m_objectCount; i++)
       {
            m_objects[i].Render();
       }
    }
    DrawingObject[] m_objects;
    int m_objectCount;
}
public class DrawingObject
{
    internal void Render() {}
}
class Test
{
    public static void Main()
    {
       DrawingObjectGroup group = new DrawingObjectGroup();
       group.AddObject(new DrawingObject());
    }
}

Here, the DrawingObjectGroup object holds up to ten drawing objects. It’s valid for the user to have a reference to a DrawingObject, but it would be invalid for the user to call Render() for that object, so this is prevented by making the Render() function internal.

image Tip  This code doesn’t make sense in a real program. The .NET Common Language Runtime has a number of collection classes that would make this sort of code much more straightforward and less error prone. See Chapter 33 for more information.

Expanding Internal Accessibility

In certain scenarios, it is useful to provide internal-level access to a class that is not in the same assembly. See the “Expanding Internal Accessibility” section in Chapter 31 for more information.

Protected

As noted in the chapter on inheritance, protected indicates that the member can also be accessed by classes that are derived from the class defining the member.

Internal Protected

To provide some extra flexibility in how a class is defined, the internal protected modifier can be used to indicate that a member can be accessed from either a class that could access it through the internal access path or a class that could access it through a protected access path. In other words, internal protected allows internal or protected access.

Note that there is no way to specify that a member can only be accessed through derived classes that live in the same assembly (the so-called internal and protected accessibility), although an internal class with a protected member will provide that level of access.1

The Interaction of Class and Member Accessibility

Class and member accessibility modifiers must both be satisfied for a member to be accessible. The accessibility of members is limited by the class so that it does not exceed the accessibility of the class.

Consider the following situation:

internal class MyHelperClass
{
    public void PublicFunction() {}
    internal void InternalFunction() {}
    protected void ProtectedFunction() {}
}

If this class were declared as a public class, the accessibility of the members would be the same as the stated accessibility; for example, PublicFunction() would be public, InternalFunction() would be internal, and ProtectedFunction() would be protected.

Because the class is internal, however, the public on PublicFunction() is reduced to internal.

Accessability Summary

The available accessability levels in C# are summarized in Table 6-1.

Table 6-1. Accessibility in C#

Accessibility Description
public No restrictions on access.
protected Can be accessed in the declaring class or derived classes.
internal Can be accessed by all types in the same assembly of the declaring class and other assemblies specifically named using the InternalsVisibleTo attribute.
protected internal Any access granted by protected or internal.
private Only accessed by the declaring class.

Method Overloading

When there are several overloaded methods for a single named function, the C# compiler uses method overloading rules to determine which function to call.

In general, the rules are fairly straightforward, but the details can be somewhat complicated. Here’s a simple example:

Console.WriteLine("Ummagumma");

To resolve this, the compiler will look at the Console class and find all methods that take a single parameter. It will then compare the type of the argument (string in this case) with the type of the parameter for each method, and if it finds a single match, that’s the function to call. If it finds no matches, a compile-time error is generated. If it finds more than one match, things are a bit more complicated (see the “Better Conversions” section below).

For an argument to match a parameter, it must fit one of the following cases:

  • The argument type and the parameter type are the same type.
  • An implicit conversion exists from the argument type to the parameter type and the argument is not passed using ref or out.

Note that in the previous description, the return type of a function is not mentioned. That’s because for C#—and for the .NET Common Language Runtime in general—overloading based on return type is not allowed.2 Additionally, because out is a C#-only construct (it looks like ref to other languages), there cannot be a ref overload and an out overload that differ only in their ref and out-ness. There can, however, be a ref or out overload and a pass by value overload using the same type, although it is not recommended.

Method Hiding

When determining the set of methods to consider, the compiler will walk up the inheritance tree until it finds a method that is applicable and then perform overload resolution at that level in the inheritance hierarchy only; it will not consider functions declared at different levels of the hierarchy.3 Consider the following example:

using System;
public class Base
{
    public void Process(short value)
    {
       Console.WriteLine("Base.Process(short): {0}", value);
    }
}
public class Derived: Base
{
    public void Process(int value)
    {
       Console.WriteLine("Derived.Process(int): {0}", value);
    }
    public void Process(string value)
    {
       Console.WriteLine("Derived.Process(string): {0}", value);
    }
}
class Test
{
    public static void Main()
    {
       Derived d = new Derived();
       short i = 12;
       d.Process(i);
       ((Base) d).Process(i);
    }
}

This example generates the following output:

Derived.Process(int): 12
Base.Process(short): 12

A quick look at this code might lead one to suspect that the d.Process(i) call would call the base class function because that version takes a short, which matches exactly. But according to the rules, once the compiler has determined that Derived.Process(int) is a match, it doesn’t look any farther up the hierarchy; therefore, Derived.Process(int) is the function called.4

To call the base class function requires an explicit cast to the base class because the derived function hides the base class version.

Better Conversions

In some situations there are multiple matches based on the simple rule mentioned previously. When this happens, a few rules determine which situation is considered better, and if there is a single one that is better than all the others, it is the one called.5

The three rules are as follows:

  1. An exact match of type is preferred over one that requires a conversion.
  2. If an implicit conversion exists from one type to another and there is no implicit conversion in the other direction, the type that has the implicit conversion is preferred.
  3. If the argument is a signed integer type, a conversion to another signed integer type is preferred over one to an unsigned integer type.

Rules 1 and 3 don’t require a lot of explanation. Rule 2, however, seems a bit more complex. An example should make it clearer:

using System;
public class MyClass
{
    public void Process(long value)
    {
       Console.WriteLine("Process(long): {0}", value);
    }
    public void Process(short value)
    {
       Console.WriteLine("Process(short): {0}", value);
    }
}
class Test
{
    public static void Main()
    {
       MyClass myClass = new MyClass();
       int i = 12;
       myClass.Process(i);

       sbyte s = 12;
       myClass.Process(s);
    }
}

This example generates the following output:

Process(long): 12
Process(short): 12

In the first call to Process(), an int is passed as an argument. This matches the long version of the function because there’s an implicit conversion from int to long and no implicit conversion from int to short.

In the second call, however, there are implicit conversions from sbyte to short or long. In this case, the second rule applies. There is an implicit conversion from short to long, and there isn’t one from long to short; therefore, the version that takes a short is preferred.

Variable-Length Parameter Lists

It is sometimes useful to define a parameter to take a variable number of parameters (Console.WriteLine() is a good example). C# allows such support to be easily added:

using System;
class Port
{
       // version with a single object parameter
    public void Write(string label, object arg)
    {
       WriteString(label);
       WriteString(arg.ToString());
    }
       // version with an array of object parameters
    public void Write(string label, params object[] args)
    {
       WriteString(label);
       foreach (object o in args)
       {
            WriteString(o.ToString());
       }
    }
    void WriteString(string str)
    {
            // writes string to the port here
       Console.WriteLine("Port debug: {0}", str);
    }
}
class Test
{
    public static void Main()
    {
       Port port = new Port();
       port.Write("Single Test", "Port ok");
       port.Write("Port Test: ", "a", "b", 12, 14.2);
       object[] arr = new object[4];
       arr[0] = "The";
       arr[1] = "answer";
       arr[2] = "is";
       arr[3] = 42;
       port.Write("What is the answer?", arr);
    }
}

The params keyword on the last parameter changes the way the compiler looks up functions. When it encounters a call to that function, it first checks to see if there is an exact match for the function. The first function call matches:

public void Write(string, object arg)

Similarly, the third function passes an object array, and it matches:

public void Write(string label, params object[] args)

Things get interesting for the second call. The definition with the object parameter doesn’t match, but neither does the one with the object array.

When both of these matches fail, the compiler notices that the params keyword was specified, and it then tries to match the parameter list by removing the array part of the params parameter and duplicating that parameter until there are the same number of parameters.

If this results in a function that matches, it then writes the code to create the object array. In other words, the line

port.Write("Port Test: ", "a", "b", 12, 14.2);

is rewritten as:

object[] temp = new object[4];
temp[0] = "a";
temp[1] = "b";
temp[2] = 12;
temp[3] = 14.2;
port.Write("Port Test: ", temp);

In this example, the params parameter was an object array, but it can be an array of any type.

In addition to the version that takes the array, it usually makes sense to provide one or more specific versions of the function. This is useful both for efficiency (so the object array doesn’t have to be created) and so languages that don’t support the params syntax don’t have to use the object array for all calls. Overloading a function with versions that take one, two, and three parameters, plus a version that takes an array, is a good rule of thumb.

Default Arguments

If a method has multiple parameters, some of them may be optional. Consider the following class:

public class Logger
{
    public void LogMessage(string message, string component)
    {
       Console.WriteLine("{0} {1}", component, message);
    }
}

To use that, we can write the following code:

logger.LogMessage("Started", "Main");

Looking at the usages of the LogMessage() method, we discover that many of them pass "Main" as the component. It would certainly be simpler if we could skip passing it when we didn’t need it, so we add an overload:

    public void LogMessage(string message)
    {
       LogMessage(message, "Main");
    }

which allows us to write:

logger.LogMessage("Started");

It would certainly be simpler if we could write that method once and not have to repeat ourselves simply to add a simpler overload. With default arguments,6 we can do the following:

    public void LogMessage(string message, string component = "Main")
    {
       Console.WriteLine("{0} {1}", component, message);
    }

This works pretty much the way you would expect it to; if you pass two arguments, it functions normally; but if you pass only one arguments, it will insert the value "Main" as the second arguments for the call.

There is one restriction to default arguments; the value that is specified has to be a compile-time constant value. If you want to use a value that is determined at runtime, you will have to use method overloading instead.

METHOD OVERLOADING VS DEFAULT ARGUMENTS

Method overloading and default arguments give the same result in most situations, so it’s mostly a manner of choosing which one is more convenient. They do, however, differ in implementation.

In the overloaded case, the default values are contained within the assembly that contains the class with the method. In the default arguments case, the default values are stored where the method is called.

In many cases this is not significant. However, if the class ships as part of an assembly that might need to be versioned—for a security update, perhaps—then the defaults in the method case can be updated by shipping a new version of the assembly, while the default arguments case can only get updated defaults by recompiling the caller.

If you are in the business of shipping libraries, the difference may matter. Otherwise, it probably isn’t significant.

Named Arguments

Named arguments allow us to specify a parameter by name instead of by position. For example, we could use it to specify the name of our component parameter:

logger.LogMessage("Started", component: "Main");

Named arguments can also be used to make code more readable. Consider the following logging class:

public class Logger
{
    public static void LogMessage(string message, bool includeDateAndTime)
    {
       if (includeDateAndTime)
       {
            Console.WriteLine(DateTime.Now);
       }
       Console.WriteLine(message);
    }
}

The class is used as follows:

Logger.LogMessage("Warp initiated", true);

If I don’t know anything about the Logger class, I can probably guess that the first parameter is the message, but it’s not clear what the second parameter is. I can, however, write it using a named argument:

Logger.LogMessage("Warp initiated", includeDateAndTime: true);

That is much clearer.7

image Tip  The named argument can also be used for the other arguments to methods, but my experience is that if you use well-named variables, it usually isn’t an issue.

1 During the design of C#, there was some discussion around whether it would make sense to provide an option that meant internal and protected. We elected not to provide it, partly because there wasn’t a well-defined need for it, and partly because there was no obvious, nonconfusing way to express it syntactically.

2 In other words, C++ covariant return types are not supported.

3 See Chapter 10 for more discussion on this behavior.

4 The reason for this behavior is fairly subtle. If the compiler kept walking up the tree to find the best match, adding a new method in a library base class could result in that method being called rather than the one in the user’s derived class, which would be bad.

5 The rules for this are detailed in section 7.4.2.3 of the C# Language Reference, which can be downloaded from MSDN.

6 Wait, isn’t this feature about default parameters? Why are you using the term “argument”? It’s quite simple. A parameter is something you define as method declaration, so in this case component is a parameter. An argument is what you pass to a parameter, "Main", in this case.

7 Another option is to define an enum. The enum has the advantage of requiring the caller to specify the name, but it is much more work to set up.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.199.27