Here's how you use it:
public void Foo (
int doodad = 1,
string humdinger = "",
string wuchacallit = "STELLAAAA!")
{
// Do stuff here
}
void Test ()
{
Foo ();
Foo (humdinger = "Beard Lust");
Foo (doodad = 5,
wuchacallit = "SHAMU!");
Foo (wuchacallit = "Kia",
humdinger = "Ora",
doodad = 42);
}
int doodad = 1,
string humdinger = "",
string wuchacallit = "STELLAAAA!")
{
// Do stuff here
}
void Test ()
{
Foo ();
Foo (humdinger = "Beard Lust");
Foo (doodad = 5,
wuchacallit = "SHAMU!");
Foo (wuchacallit = "Kia",
humdinger = "Ora",
doodad = 42);
}
Here's how it really works:
public void Foo (
[Optional, DefaultParameterValue(1)]
int doodad,
[Optional, DefaultParameterValue("")]
string humdinger,
[Optional, DefaultParameterValue("STELLAAAA!")]
string wuchacallit)
{
// Do stuff here
}
void Test ()
{
Foo (1, "", "STELLAAAA!");
Foo (1, "Beard Lust", "STELLAAAA!");
Foo (5, "", "SHAMU!");
Foo (42, "Ora", "Kia");
}
[Optional, DefaultParameterValue(1)]
int doodad,
[Optional, DefaultParameterValue("")]
string humdinger,
[Optional, DefaultParameterValue("STELLAAAA!")]
string wuchacallit)
{
// Do stuff here
}
void Test ()
{
Foo (1, "", "STELLAAAA!");
Foo (1, "Beard Lust", "STELLAAAA!");
Foo (5, "", "SHAMU!");
Foo (42, "Ora", "Kia");
}
The compiler just sprinkles the default values into every callsite. There are a few problems with this approach. Let's suppose I release version 1 of my awesome library with the above Foo method. You compile. All is well. Now let's say I release version 2 of my library, in which the default value "STELLAAAA!" is changed to "KHAAAAN!". But your code still has the old default value baked in. You need to re-compile your code to get the new default value. There is also the problem that injecting the full argument list into every callsite bloats the size of the code. Bigger code means more to JIT and fewer cache hits
How it should work:
struct FooSettings {
int doodad_value = 1;
string humdinger_value = "";
string wuchacallit_value = "STELLAAAA!";
public int doodad {
get { return doodad_value; }
set { doodad_value = value; }
}
public string humdinger {
get { return humdinger_value; }
set { humdinger_value = value; }
}
public string wuchacallit {
get { return wuchacallit_value; }
set { wuchacallit_value = value; }
}
}
public void Foo (FooSettings settings)
{
// Do stuff
}
void Test ()
{
Foo (new FooSettings ());
Foo (new FooSettings { humdinger = "Beard Lust" });
Foo (new FooSettings {
doodad = 5,
wuchacallit = "SHAMU!" });
Foo (new FooSettings {
wuchacallit = "Kia",
humdinger = "Ora",
doodad = 42 });
}
int doodad_value = 1;
string humdinger_value = "";
string wuchacallit_value = "STELLAAAA!";
public int doodad {
get { return doodad_value; }
set { doodad_value = value; }
}
public string humdinger {
get { return humdinger_value; }
set { humdinger_value = value; }
}
public string wuchacallit {
get { return wuchacallit_value; }
set { wuchacallit_value = value; }
}
}
public void Foo (FooSettings settings)
{
// Do stuff
}
void Test ()
{
Foo (new FooSettings ());
Foo (new FooSettings { humdinger = "Beard Lust" });
Foo (new FooSettings {
doodad = 5,
wuchacallit = "SHAMU!" });
Foo (new FooSettings {
wuchacallit = "Kia",
humdinger = "Ora",
doodad = 42 });
}
Thanks to C# 3's object initialization, you can squint at the call and almost see the named parameter syntax (just ignore "new FooSettings"). This pattern of using a special "settings" type for passing arguments to methods already exists in the framework (see XmlReader.Create and XmlWriter.Create for an example). I am proposing that the compiler auto-generate these types and provide full optional/named parameter sugar. The compiler-generated types would be publicly nested within the type containing the method and named "[MemberName]Settings" by default.
This is better than callsite default value injection because:
- It versions well
- It adds a fixed amount of additional code (the type), whereas injection adds more code every time you use it
- It is CLS compliant
This is how C# 4 should do optional and named parameters.
17 comments:
Fully agreed here
Not to mention the tension between default arguments and polymorphism (a base class can define a function with a default argument value, whereas a derived a class can override that function, optionally using a different default. It quickly becomes nearly impossible to tell which default value gets used when calling the polymorphic method).
Note that this issue stems from my experience with C++, so it could be that C# (.NET) actually enforces consistency across class hierarchies?
Polymorphism is a bit tricky. I am using a struct for the generated type so that it cannot be null (and nothing is allocated on the heap). This means you can't have inheritance from a *Settings type. Another option would be: define an interface type for use in the (generated) virtual method sig. If a derived class wants to provide its own defaults, it can have its own nested settings type which implements that interface. The compiler selects the appropriate value type. If you really wanted to make sure that nobody could ever use a settings type designed for something else in the inheritance chain, then the virtual method (with the interface parameter) could be protected and a public method which takes the exact value type could call through to the protected method.
All of this extra jazz with interfaces is only necessary if the method is virtual, obviously.
You may well be able to get part of the way you want to with PostSharp.
Why not simply do:
public void Foo ( int doodad, string humdinger, string wuchacallit )
{
// Do stuff here
}
public void Foo ( int doodad, string humdinger )
{
Foo( doodad, humdinger, "STELLAAAA!" );
}
public void Foo ( int doodad )
{
Foo( doodad, String.Empty, "STELLAAAA!" );
}
public void Foo ()
{
Foo( 1, String.Empty, "STELLAAAA!" );
}
And as a consumer you would simply choose the proper method?
Why do we need optional and default parameters at all?
@Kriz: I'm right there with you on being a little skeptical of optional/named parameters. In fact, Rico Mariani makes the same proposal you do in Framework Design Guidelines about using default values as a way of auto-generating overloads.
The problem is, how do you do the overload which just takes humdinger, and the one which just takes wuchacallit. Both are strings, so there is no way to have overloads for each. Also, when you get into the realm of a lot of parameters (which is where optional parameters are most useful) the overloads are tricky or impossible to work out.
As far as I can tell, optional and named parameters are a "listening to our customers" feature. Everybody wants then, apparently. I'm really of the opinion that you shouldn't need them in the language: either do overloads, or a *Settings type. But since the masses are clamoring for the feature (whether as a result of the Office APIs or not - don't get me started), it is going to be in C# 4. I'm just offering a better alternative to callsite injection.
@Kriz: Not to mention that overloading is in fact a language feature that doesn't keep a clear semantic some of the time when generics are involved.
Figuring out which method overload would be called for a generic static method with parameter covariance, polymoprhic parameter type and a generic containing class...
You don't really want to be in that space unless you really wanted to C++ instead :)
One of the reasons they said they were adding this at PDC was they want C# and VB.Net to have feature parity. This is something that VB.Net has had I think since the beginning.
This causes a problem if you have a VB.Net method with optional parameters that you try to call from C#.
I wonder if it is implemented this way in VB.Net and so the C# implementation has to be the same?
VB does use callsite injection for optional parameters, but you can still call VB methods w/ optional parameters using C# today - you just need to fill in all of the arguments yourself.
Passing a structure is not in
any way faster/smaller than
passing a bunch of parameters.
Together with the new C# 4.0 "dynamic" keyword/late binding, optional parameters as designed by MS are very useful for consuming COM objects.
Your approach won't be COM compatible.
There is no reason you couldn't have optional and named parameter support in the dynamic member provider interface as well. I don't see how this approach has any effect on the use of the dynamic keyword.
There is bigger problem with named arguments.
Simple argument renaming will break API compatibility. Also simple interface contracts are now not enough, implementing an interface using different parameter names can break contract from C# perspective and the list of issues can obviously go on and on.
You *should* be required to recompile to take advantage of a new default value. Though, admittedly, you probably shouldn't be changing default values to begin with.
The problem is that if you change the default value in the way you propose, you've have effectively rewritten everyone else's code. What if they specifically *wanted* the value that was the default? What if they *required* that value?
Default values shouldn't be published lightly. They shouldn't be changed, certainly. They definitely aren't (nor should be) interpreted as a situation where the dev doesn't care about the value.
@Kriz, etc: Consider trying to use C# with COM and the Office APIs in particular. The alternative is precisely what you suggest, but goes into the dozens of paramters (only very slight exaggeration, if at all).
@Keith: Default values are the purview of the party writing the function. If a consumer uses a default value, they want to use "the default value," not "the string literal 'STELAAAA!' which, at the time of compilation, was the default value." Consider method overloads. If I provide an overload with fewer parameters which calls through to an overload with more parameters, passing "default" values, then those values are under my full control. I can change the default values and all release a new version of my assembly and all code which consumes it will use the new default values w/o recompilation. This is just how the concept of default values works. If the consumer code actually wants to use a particular literal value, they can specify it. If they want to use whatever the "default" value is for a parameter, then that value must be under the control of whoever wrote the function - they are the one who knows what the "default" value should be.
Say that all you want, that's *not* how it will get used :)
Consider that a company will spend vast sums just to certify their app, which may just happen to use default values regardless of how you or they interpret it. If you decide to break your contract by changing the default value, you just wasted their time and money. I wouldn't want to take a dependency on that.
Default values represent a contract in that regard, which shouldn't be broken. Recompiling is a trivial means of protecting against such breaches.
Changing the default value does not break the contract. It is exactly like changing the default value passed from an overload with fewer parameters to one with more.
If you take issue with the larger problems of having optional/named parameters in a public API, I would probably agree with you. I'm not really a fan of the language feature. But it is coming to C# 4 regardless.
This won't succeed in reality, that is exactly what I think.
Post a Comment