Hiding Methods With The “new” Modifier

We were discussing the use of the new modifier in C# in my team today, and I was expressing my opinion that I don't like it. The new modifier is used to explicitly hide a method in a base class. I'm sure there are good reasons to do this, but generally the use of the new modifier rings alarm bells for me that the API design is wrong. If you're hiding and re-defining properties and methods using new in a public API then you could trip up consumers of your API and introduce subtle bugs into their code.

It all has to do with predictability and consistency in API usage. Let me explain.

Take the following type...

/// <summary>

/// Base type from which others will derive

/// </summary>

public class Base

{

/// <summary>

/// A non-virtual method that we weren't expecting anyone to override

/// </summary>

public void NonVirtualMethod()

{

Console.WriteLine("Hello from Base");

}

/// <summary>

/// A virtual method that we expect derived types to override

/// </summary>

public virtual void VirtualMethod()

{

Console.WriteLine("Hello from Base");

}    

}

This type declares two methods – NonVirtualMethod and VirtualMethod. The developer who designed this type wasn't expecting that anyone would need to override NonVirtualMethod. They did, however, expect that VirtualMethod might be overridden in a derived type.

Now we define a type that derived from Base...

/// <summary>

/// Type that derives from Base

/// </summary>

public class Derived : Base

{

/// <summary>

/// We're hiding NonVirtualMethod in Base and providing our own

/// </summary>

public new void NonVirtualMethod()

{

Console.WriteLine("Hello from Derived");

}

/// <summary>

/// We're overriding VirtualMethod to provide our own implementation

/// </summary>

public override void VirtualMethod()

{

Console.WriteLine("Hello from Derived");

}

}

I've highlighted the new modifier. The type Derived explicitly hides the base classes NonVirtualMethod and supplies its own. C# forces you to be explicit about this (which is good). This type also overrides the VirtualMethod using the override modifier.

Now, along comes a consumer of this API and writes the following code:

public static void Main(string[] args)

{

// create instance of Derived type

Derived foo = new Derived();

// reference the same instance of Derived using its base type Base

Base bar = foo;

// call the virtual method                                         

foo.VirtualMethod();

bar.VirtualMethod();

// call non-virtual method

foo.NonVirtualMethod();

bar.NonVirtualMethod();

// wait for use to hit a key

Console.WriteLine("Press any key to continue...");

Console.Read();

}

Now, can you tell me what you'd expect to see written to the Console here? I've created a single instance of the type Derived and called VirtualMethod and NonVirtualMethod in 2 ways; first using a reference declared as Derived and second through a reference declared as Base.

When you see on the console is this...

Hello from Derived

Hello from Derived

Hello from Derived

Hello from Base

Press any key to continue...

When we call VirtualMethod it doesn't matter how we call it – I get the implementation in the type Derived. This, I would suggest, is pretty obvious and predictable to most developers.

However, when NonVirtualMethod is called we get two different responses. It turns out that it makes a difference how we call NonVirtualMethod. When we call it via a reference of type Derived we get the implementation in the type Derived. When we call it using a Base type reference we get the implementation in Base.

This behaviour is totally understandable and there's nothing wrong with it, but for the consumer of this API was it expected? Are they expecting that an instance of Derived will behave differently when cast to its base type? When dealing with virtual methods you get consistent behaviour regardless of whether you're casting to the base type or not.

So, I would argue that using new in a public API is a bad thing. It could lead to subtle bugs within code that consumes your API and, as such, should be avoided.