C Sharp

Attribute Parameters

In the examples above, I covered the use of attaching attributes by way of their constructors. Now I'll look at some issues regarding attribute constructors that I didn't cover earlier.

Positional Parameters and Named Parameters

In the FieldAttrApp example in the previous section, you saw an attribute named RegistryKeyAttribute. Its constructor looked like the following: -

public RegistryKeyAttribute(RegistryHives Hive, String ValueName) -

Based on that constructor signature, the attribute was then attached to a field like so: -

[RegistryKey(RegistryHives.HKEY_CURRENT_USER, "Foo")]
public int Foo;

So far, this is straightforward. The constructor has two parameters, and two parameters were used in the attaching of that attribute to a field. However, we can make this easier to program. If the parameter is going to remain the same the majority of the time, why make the class's user type it in each time? We can set default values by using positional parameters and named parameters.

Positional parameters are parameters to the attribute's constructor. They are mandatory and must be specified every time the attribute is used. In the RegistryKeyAttribute example above, Hive and ValueName are both positional parameters. Named parameters are actually not defined in the attribute's constructor-rather, they are nonstatic fields and properties. Therefore, named parameters give the client the ability to set an attribute's fields and properties when the attribute is instantiated without you having to create a constructor for every possible combination of fields and properties that the client might want to set.

Each public constructor can define a sequence of positional parameters. This is the same as any type of class. However, in the case of attributes, once the positional parameters have been stated, the user can then reference certain fields or properties with the syntax FieldOrPropertyName=Value. Let's modify the attribute RegistryKeyAttribute to illustrate this. In this example, we'll make RegistryKeyAttribute.ValueName the only positional parameter, and RegistryKeyAttribute.Hive will become an optional named parameter. So, the question is, "How do you define something as a named parameter?" Because only positional-and, therefore, mandatory-parameters are included in the constructor's definition, simply remove the parameter from the constructor's definition. The user can then reference as a named parameter any field that is not readonly, static, or const or any property that includes a set accessor method, or setter, that is not static. Therefore, to make the RegistryKeyAttribute.Hive a named parameter, we would remove it from the constructor's definition because it already exists as a public read/write property: -

public RegistryKeyAttribute(String ValueName)

The user can now attach the attribute in either of the following ways: -

[RegistryKey("Foo")]
[RegistryKey("Foo", Hive = RegistryHives.HKEY_LOCAL_MACHINE)]

This gives you the flexibility of having a default value for a field while at the same time giving the user the ability to override that value if needed. But hold on! If the user doesn't set the value of the RegistryKeyAttribute.Hive field, how do we default it? You might be thinking, "Well, we check to see whether it has been set in the constructor." However, the problem is that the RegistryKeyAttribute.Hive is an enum with an underlying type of int-it is a value type. This means that by definition the compiler has initialized it to 0! If we examine the RegistryKeyAttribute.Hive value in the constructor and find it to be equal to 0, we don't know if that value was placed there by the caller through a named parameter or if the compiler initialized it because it's a value type. Unfortunately, at this time, the only way I know to get around this problem is to change the code such that the value 0 isn't valid. This can be done by changing the RegistryHives enum as follows: -

public enum RegistryHives
{
    HKEY_CLASSES_ROOT = 1,
    HKEY_CURRENT_USER,
    HKEY_LOCAL_MACHINE,
    HKEY_USERS,
    HKEY_CURRENT_CONFIG
}

Now we know that the only way RegistryKeyAttribute.Hive can be 0 is if the compiler has initialized it to 0 and the user did not override that value via a named parameter. We can now write code similar to the following to initialize it: -

    public RegistryKeyAttribute(String ValueName)
    {
        if (this.Hive == 0)
            this.Hive = RegistryHives.HKEY_CURRENT_USER;
        this.ValueName = ValueName;
    }

Common Mistakes with Named Parameters

When using named parameters, you must specify the positional parameters first. After that, the named parameters can exist in any order because they're preceded by the name of the field or property. The following, for example, will result in a compiler error: -

// This is an error because positional parameters can't follow
// named parameters.
[RegistryKey(Hive=RegistryHives.HKEY_LOCAL_MACHINE, "Foo")]

Additionally, you can't name positional parameters. When the compiler is compiling an attribute's usage, it will attempt to resolve the named parameters first. Then it will attempt to resolve what's left over-the positional parameters-with the method signature. The following will not compile because the compiler can resolve each named parameter but, when finished with the named parameters, it can't find any positional parameters and states that " No overload for method 'RegistryKeyAttribute' takes '0'' arguments ": -

[RegistryKey(ValueName="Foo", Hive=RegistryHives.HKEY_LOCAL_MACHINE)]

Lastly, named parameters can be any publicly accessible field or property-including a setter method-that is not static or const.