Member Accessibility and Overloading in C#

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

1. Class Accessibility

The coarsest level at which accessibility can be controlled is at the class. In most cases, the only valid modifiers on a class are public, which means 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 8.

The internal modifier is a way of granting access to a wider set of classes without granting access to everybody, and it’s 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.

2. Using internal on Members

You can also use the internal modifier 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()

{

objects = new DrawingObject[l0];

objectCount = 0;

}

public void AddObject(DrawingObject obj)

{

if (objectCount < 10)

{

objects[objectCount] = obj;

objectCount++;

}

}

public void Render()

{

for (int i = 0; i < objectCount; i++)

{

objects[i].Render();

}

}

DrawingObject[] objects; int        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’d be invalid for the user to call Render() for that object, so this is prevented by making the Render() function internal.

3. internal protected

To provide some extra flexibility in how a class is defined, you can use the internal protected modifier 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’s no way to specify internal and protected in C#, though an internal class with a protected member will provide that level of access.

4. 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 doesn’t exceed the accessibility of the class.

Consider the following situation:

internal class MyHelperClass {

public void PublicFunction() {}

internal void InternalFunction() {}

protected void ProtectedFunction() {}

}

If you declared this class as a public class, the accessibility of the members would be the same as the stated accessibility; in other words, 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.

5. Method Overloading

When a single-named function has several overloaded methods, 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).

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 isn’t passed using ref or out.

Note that in the previous description, the return type of a function isn’t mentioned. That’s because for C#—and for the .NET CLR—overloading based on return type isn’t allowed.[1] Addition­ally, because out is a C#-only construct (it looks like ref to other languages), there can’t be a ref overload and an out overload that differ only in their ref and outness. There can, however, be a ref or out overload and a pass-by-value overload.

5.1. Method Hiding

When determining the set of methods to consider, the compiler will walk up the inheritance tree until it finds a method that’s applicable and then perform overload resolution at that level in the inheritance hierarchy only; it won’t consider functions declared at different levels of the hierarchy. 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 further up the hierarchy; therefore, Derived.Process(int) is the function called.

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

5.2. 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’s better than all the others, it’s the one called.[2] The three rules are as follows:

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

The first and third rules don’t require a lot of explanation. The second, 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 conver­sion 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’s 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.

6. Variable-Length Parameter Lists

It’s 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’s 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 is present, 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 following line:

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

is rewritten as follows:

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.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *