C# Coding Solutions—Understanding the Overloaded Return Type and Property

Microsoft .NET Framework, ASP.NET, Visual C# (CSharp, C Sharp, C-Sharp) Developer Training, Visual Studio


Jump to: navigation, search
CSharp-Online.NET:Articles
C# Articles

C# Coding Solutions

© 2006 Christian Gross

Understanding the Overloaded Return Type and Property

Recently someone came to me wanting to define two classes that related to each other. The relation between the two classes would be defined as a common architecture, and based on those two classes two specialized classes would be defined that would have the same relation. The problem is that the person wanted to use the exact same access code to access the new specialized types. In .NET you cannot define two methods with the same parameter signature but different return types. In this section I’ll show you how to solve the problem by using the overloaded return type and property.

The following code is disallowed because the two variations of GetValue have the same signature but different return values:

class Illegal {
    public int GetValue() {
    }
    public double GetValue() {
    }
}
// …
Illegal cls = new Illegal();
cls.GetValue(); // Which variation is called? 

At the bottom of the code example the class Illegal is instantiated and the method GetValue is called. Because GetValue’s return value is not assigned, the compiler has no idea which variation of GetValue to call. The solution is the following code:

class Legal {
    public int GetValueInt() {
    }
    public double GetValueDouble() {
    }
}

The methods have been relabeled to be unique, and thus each can return whatever it wants. However, the client needs to know that the two methods exist, and it needs to call the correct version. Going back to the original variation where the method names were identical, if the return value were assigned to a variable, then the compiler could figure out which variation of the method is called. The compiler does not allow such decision-making. However, the compiler does allow some things that boggle the mind. For example, the following code is completely legal:

class BaseIntType {
    public int GetValue() {
        Console.WriteLine( "BaseIntType.GetValue int");
        return 0;
    }
}
class DerivedDoubleType : BaseIntType {
    public double GetValue() {
        Console.WriteLine( "DerivedDoubleType.GetValue double");
        return 0.0;
    }
}
// Client code
new BaseIntType().GetValue();
new DerivedDoubleType().GetValue();
((BaseIntType)new DerivedDoubleType()).GetValue();

Using inheritance and having two methods return different types is legal, yet putting those definitions in the same class is not. You can understand the reasoning by looking at the IL used to call the types:

.method public hidebysig instance void  Test() cil managed
{
  .custom instance void [nunit.framework]NUnit.Framework.
TestAttribute::.ctor() = ( 01 00 00 00 )
  // Code size       34 (0x22)
  .maxstack  8
  IL_0000:  newobj     instance void BaseIntType::.ctor()
  IL_0005:  call       instance int32 BaseIntType::GetValue()
  IL_000a:  pop
  IL_000b:  newobj     instance void DerivedDoubleType::.ctor()
  IL_0010:  call       instance float64 DerivedDoubleType::GetValue()
  IL_0015:  pop
  IL_0016:  newobj     instance void DerivedDoubleType::.ctor()
  IL_001b:  call       instance int32 BaseIntType::GetValue()
  IL_0020:  pop
  IL_0021:  ret
} // end of method Tests::Test

The compiler used the contract rules defined by the object hierarchy and associated the method with the type that calls the GetValue method. This works so long as the virtual and override keywords are not used with the method GetValue. The compiler treats this inheritance situation as unique. For example, the following declaration for DerivedDoubleType would be illegal:

class DerivedDoubleType : BaseIntType {
     public new int GetValue() { return 0;}
     public double GetValue() {
            return 0.0;
     }
}

The compiler allows the definition of return types at different levels of the inheritance. But you cannot use that information because the code is bound to a particular implementation defined at the same level as the class that is instantiated. The compiler is incapable of going up the inheritance hierarchy and figuring out the available return-value variations.

We can use parameter declarations to get around this return-value problem; for example, GetValue with int has one parameter, and GetValue for double has two parameters. The compiler knows which method to call when dealing with an overloaded method. However, that solution is silly in that you still need to know whether to call GetValue with one parameter or two.

Another attempt is the following source code, which illustrates the same situation as the method examples, except it uses properties:

class User {
    public void Method() {
        Console.WriteLine( "User.Method");
    }
}
 
class Session {
    protected User _myUser = new User();
    public User MyUser {
        get {
            return _myUser;
        }
    }
}
class UserSpecialized : User {
    public new void Method() {
        Console.WriteLine( "UserSpecialized.Method");
    }
}
class SessionSpecialized : Session {
    public SessionSpecialized() {
        _myUser = new UserSpecialized();
    }
    public UserSpecialized MyUser {
        get {
            return _myUser as UserSpecialized;
        }
    }
}

There are two base classes: Session and User. Session exposes the MyUser property, which is of type User. The class SessionSpecialized derives from the class Session, and the class UserSpecialized derives from User. The class SessionSpecialized also exposes a MyUser property, but it is of type UserSpecialized.

We have two properties of different types in a hierarchy. To illustrate which methods are called, the classes could be called as follows:

new Session().MyUser.Method();
new SessionSpecialized().MyUser.Method();
((Session)new SessionSpecialized()).MyUser.Method();

The first line should call User.Method because a type Session is instantiated where Session exposes MyUser as type User. For the second line, the generated output should be UserSpecialized.Method because SessionSpecialized instantiates the type UserSpecialized. The third line poses a more serious challenge because SessionSpecialized is instantiated and downcast to Session. Because no virtual or override keywords were used, downcasting to Session means referencing a User instance, and it causes User.Method to be called when generating output. The generated output is as follows:

User.Method
UserSpecialized.Method
User.Method

The generated output is what we expected. This means that unlike with methods, it is possible to overload properties and then reference them arbitrarily. This works as long as you respect the rules of not overwriting the properties. The .NET compiler returns the property associated with the type being queried.

So far everything seems to work, but there is still a nagging problem. Inheritance is an interesting way to create methods or properties that return different types, but it can create a conflict with respect to a value type. For example, the following code would generate a compiler failure:

int value = new DerivedDoubleType().GetValue();

To make the code work, a downcast to BaseIntType would be necessary before assigning value. Not ideal, but possible. Another solution is a bit more complicated and involves the use of .NET Generics.

The complete solution is illustrated as follows, minus the definitions for User and UserSpecialized, as they do not change from the previous declarations:

class Session {
    protected User _myUser = new User();
    public virtual type MyUser< type>() where type: class {
        if (typeof( User).IsAssignableFrom(typeof( type))) {
            Console.WriteLine( "Is User");
            return _myUser as type;
        }
        else {
            Console.WriteLine( "Could not process");
            return null;
        }
    }
}
class SessionSpecialized : Session {
    UserSpecialized _myUserSpecialized = new UserSpecialized();
    public SessionSpecialized() {
        _myUser = _myUserSpecialized;
    }
    public override type MyUser< type>() {
        if (typeof( UserSpecialized).IsAssignableFrom(typeof( type))) {
            Console.WriteLine( "Is UserSpecialized");
            return _myUserSpecialized as type;
        }
        else {
            Console.WriteLine( "Delegated");
            return base.MyUser< type>();
        }
    }
}

The new code uses method declared .NET Generics and converts the properties into a method. Sadly, it is not possible to declare properties that use .NET Generics. Using .NET Generics at the method level enables a very flexible mechanism to determine the parameter type at runtime.

The idea behind using .NET Generics at the method level is to enable the implementation of the method to decide whether or not a certain type could be returned. For example, if in the derived class a requested type cannot be returned, then the derived class will call the base class. And then the base class will determine if the certain type can be returned. In the source code, the MyUser method has no parameters and uses .NET Generics to define the return type. The method is declared using the virtual keyword, which enables a derived class to overload the method. In the case of SessionSpecialized the MyUser method is overloaded.

Going through a scenario, when the virtual and override keywords are used, calling either Session.MyUser or SessionSpecialized.MyUser when SessionSpecialized has been instantiated results in SessionSpecialized.MyUser being called first. Calling SessionSpecialized.MyUser results in the IsAssignableFrom method being called. The method IsAssignableFrom is used to test if one type can be assigned to another type. In the case of SessionSpecialized.MyUser, the implementation is tested if the type can be cast to UserSpecialized, and if so, then it returns the instance _myUserSpecialized. If not, then you call the base class Session. In the implementation of Session.MyUser the method IsAssignableFrom is used again, but this time the type User is tested. If the test is successful, then the instance _myUser is returned. Otherwise, a null value is generated indicating that something went wrong. Another option to indicate that something went wrong is to use an exception.

Using virtual methods and calling the base class ensures that if a conversion is possible it will be made:

Session session = new SessionSpecialized();
User user = session.MyUser< User>();
UserSpecialized userSpecialized =
       session.MyUser< UserSpecialized>();

In the example source code the type SessionSpecialized is instantiated. The instantiated instance is assigned to the variable session, which is a downcast. Then the method MyUser is called twice with different types for the .NET Generics parameter (User and UserSpecialized). Calling the method MyUser will first call SessionSpecialized.MyUser because of the virtual method hierarchy. If SessionSpecialized.MyUser cannot fulfill the needs of the .NET Generics type, then SessionSpecialized.MyUser calls the base class method Session.MyUser.

Using .NET Generics, we can write methods that overload the return parameter using a .NET inheritance hierarchy that we are used to. Remember the following points when using overloaded return types:

  • The compiler cannot cope with two methods that have the same signature but different return types being declared in a single type.
  • The compiler can cope with two methods having the same signature and different return types so long as they are declared in different places in the hierarchy chain. The compiler uses type-to-method or type-to-property cross-referencing. When declaring these types of methods, you cannot use keywords such as virtual and override.
  • The safest and most flexible technique is to use .NET Generics and declare the return type as a .NET Generics parameter. It is not possible to declare properties using this technique.
  • Using .NET Generics at the method level allows a piece of code to determine the type at runtime and make decisions based on the runtime.


Previous_Page_.gif Next_Page_.gif


Personal tools