Optional argument corner cases, part two

Optional argument corner cases, part two

Rate This
  • Comments 11

(This is part two of a series on the corner cases of optional arguments in C# 4. Part one is here. Part three is here. This portion of the series was inspired by this StackOverflow question.)

Last time we saw that the declared optional arguments of an interface method need not be optional arguments of an implementing class method. That seems potentially confusing; why not require that an implementing method on a class exactly repeat the optional arguments of the declaration?

Because the cure is worse than the disease, that's why.

First off, we saw last time that one method on a class could implement two methods of two different interfaces:

interface IABC
{
  void M(bool x = true);
}

interface IXYZ
{
  void M(bool x = false);
}

class C : IABC, IXYZ
{
  public void M(bool x) {}
}

If we require the implementation to repeat the default values then the methods cannot be implicitly implemented; at least one will have to be explicitly implemented.

However, that's a rare case and we can probably dismiss it as unlikely. There are other problems though.

Suppose you have an interface in your code:

public interface IFoo
{
  void M(int x, string y, bool z);
}

and a hundred different classes that implement it. Then you decide that you want to say

public interface IFoo
{
  void M(int x, string y, bool z = false);
}

Do you really want to have to change a hundred different declarations of the implementing method? That seems like a lot of burden to put on the developer, but we could do it.

Suppose we did. Now suppose that interface IFoo is not defined in your source code, but rather is provided to you by a third party. The third party makes a new version of their interface that has a default value for the parameter z. Now when all of their thousands of customers recompile, all of those thousands of customers have to update their source code to match the default parameter! Requiring this redundancy causes the introduction of a default value to become a potentially large compilation-breaking change.

That's bad. But wait, it gets worse. Suppose you have this situation. IFoo is provided by third party FooCorp.  Base class Bar is provided by third party BarCorp:

public class Bar
{
  public void M(int x, string y, bool z) { ... }
}

Note that Bar does not implement IFoo. You wish to use both FooCorp and BarCorp code in your assembly, where you say:

class Mine : Bar, IFoo
{
}

(The compiler allows this because M on Bar is a member of Mine, and therefore Mine implicitly implements IFoo.M.)

Now FooCorp ships a new version of their assembly with the default value on z. You recompile Mine and it tells you that no, you can't do that because Bar doesn't have a matching default value for z. But you didn't write Bar! What are you supposed to do, call up BarCorp and ask them to ship you a new assembly just because FooCorp -- their competitor -- added a default value to a formal parameter of an interface? What are you supposed to do if BarCorp refuses? The method isn't even virtual, so you can't override it. The solution is ugly:

class Mine : Bar, IFoo
{
  public new void M(int x, string y, bool z = false)
  {
    base.M(x, y, z);
  }
}

Best to simply not require the redundancy in the first place.

Next time: making an argument optional does not change the signature of the method

(This is part two of a series on the corner cases of optional arguments in C# 4. Part one is here. Part three is here.)

  • Are there real world scenarios you use to justify design decisions, or is part of the design process coming up with contrived and hypothetical scenarios that could potentially break some arbitrary code?

    If the latter, how do you weigh the pro's and con's of something so contrived?

    Whatever the process is, it sure sounds like fun!

    We take all the scenarios seriously, but some are more important than others. There are always some obviously contrived scenarios that are built for the sole purpose of demonstrating a weakness in the compiler or the type system, but are very unlikely to ever be encountered by professional programmers solving real-world problems. But the sorts of "versionability" scenarios I describe here we take very seriously indeed. C# is designed to enable development of shared, versioned software components and that means taking into account all kinds of breaking change scenarios.

    Look at it this way: if a particular breaking scenario is only encountered by a few tenths of one percent of professional C# programmers, that is potentially thousands of customers with serious, real-money-is-being-wasted-by-this-nonsense business problems to solve. We want to be very conservative about introducing new potential versioning breaking change scenarios, even obscure ones.

    And yes, it is fun! -- Eric

  • @Patrick - I wouldn't consider the scenarios Eric provided as being all that contrived.  Imagine FooCorp = Microsoft, IFoo = IWin32Window and BarCorp = any .NET Winforms component provider.

    IMO, default arguments are mostly evil and should never be used in new code. They do serve one very important purpose: interfacing with existing code (like the Office APIs) that makes extensive use of default arguments.  For new code, I favor overloads, or, if the number of defaultable arguments is large, the use of a Context or Parameters class that's built up by the caller (and sets suitable defaults at construction time) rather than a large number of defaulted parameters.

  • "Now suppose that interface IFoo is not defined in your source code, but rather is provided to you by a third party. The third party makes a new version of their interface that has a default value for the parameter z. Now when all of their thousands of customers recompile, all of those thousands of customers have to update their source code to match the default parameter! Requiring this redundancy causes the introduction of a default value to become a potentially large compilation-breaking change."

    It already is, because the author of the library providing IFoo cannot assume that implementor will be C#. If it is VB, then changing the value of an optional argument is already a major breaking change (and has been since VB7, IIRC).

    For the same reason, changing the names of method parameters has been a breaking change for a long time now - C# doesn't care, but VB does, and as a library author you cannot assume which one the API client will use.

  • I'm with Carl - these examples don't seem that contrived to me.  As I recall, C# has single inheritance on classes for a reason.

    You can also look at a typical business application and see a lot of similarly-named methods on a lot of unrelated classes - how many classes have a method called Create() for example?

    There are situations where optional arguments / default parameters are useful, but they should definitely be used with care.

  • @CarlD, @John - I did not imply Eric's particular example were contrived. I was curious how and if hypothetical and contrived scenarios play a part in the compiler design process.

  • @Patrick --It is very difficult to read your comment as not implying these examples were contrived. Would you be asking if there were real world scenarios used in these decisions if you thought the post you're responding to contained such real world scenarios?

    Clearly the examples I gave are "contrived" in the non-perjorative sense. I contrived them for the purpose of the blog post. A realistic example would not involve foos and bars and M's and x's. The point of the contrivance is to strip away the unnecessary "real world" details to get at the heart of the problem. 

    The question at hand I think is not whether these particular examples are "contrived", but rather whether a contrived example that bears no resemblance whatsoever to real code is relevant. As an example, we might be exploring the question "should it be legal to have a type whose constructors become ambiguous under some generic construction?"

    class C<T> { public C(string s) {} public C(T t) {} }

    This could be a problem if the type is instantiated as C<string>; now there are two constructors that take a string. Originally in C# 2 that was going to be made illegal, but we can contrive a more realistic example:

    class SerializableCollection<T>(public SerializableCollection(Stream s) { ... } public SerializableCollection(T t) { ... } }

    Now this is much closer to a realistic example; one can easily see that you might want to construct an object from its serialized state, or to create a single-element collection. It seems like the benefit of disallowing this because someone might say SerializableCollection<Stream> is not very compelling.

    One can also think of rather more extremely contrived examples. For example, I talked a while back about how the compiler can crash and die with an "expression was too complex to compile" error when you do thinks like declare covariant interface types of the form IC<X> : IN<IN<IC<IC<X>>>>>.  No one does that. The example is contrived purely for the purpose of illustrating a hole in the type system analysis.

    -- Eric

  • It's cool to see how a very basic IT lesson (try to avoid redundant information) can impact even something complex like a compiler.

  • @Mike - If a post (not this post, an arbitrary one) is based on "real-world" scenarios, I would not ask if "real-world" scenarios played a part in the derivation of the post since that would be tautological.

    Although I did not mean to imply Eric's examples were contrived, they appear artifical (at least superficially), so one could call them "contrived".

  • Patrick, you are not making any sense.

  • A tad wordy perhaps, but he does make sense.

    Do optional parameters create a bigger problem than they solve?

  • Having written possibly hundreds of wrapper methods solely because C# doesn't have optional parameters, I have to say that I like having them around.

    But it seems like a reasonable compromise, instead of emitting an error when optional parameters don't match the interface, is to emit a warning when optional parameters don't match the interface. But perhaps the situation is not worthy of a warning just because an argument has been changed from optional to required (perhaps the developer has a good reason to write his code that way). However, if the interface says the default value is "-1" but the class says the default value is "0", that's potentially dangerous since merely changing a variable declaration between "Foo" and "IFoo" silently changes the code's meaning.

Page 1 of 1 (11 items)