Until C# 3.0, the only real solution to this was to write one or more factory methods. These are described in the sidebar below. But now we have another option.
With C# 3.0 the language was extended to support
object initializers—an extension to the new
syntax that lets us set up a load of
properties, by name, as we create our object instance.
Example 3-35 shows how an object
initializer looks when we use it in our Main
function.
Example 3-35. Using object initializers
static void Main(string[] args) { Plane someBoeing777 = new Plane("BA0049") { Direction = DirectionOfApproach.Approaching, SpeedInMilesPerHour = 150 }; Console.WriteLine( "Your plane has identifier {0}," + " and is traveling at {1:0.00}mph [{2:0.00}kph]", // Use the property getter someBoeing777.Identifier, someBoeing777.SpeedInMilesPerHour, someBoeing777.SpeedInKilometersPerHour); someBoeing777.SpeedInKilometersPerHour = 140.0; Console.WriteLine( "Your plane has identifier {0}," + " and is traveling at {1:0.00}mph [{2:0.00}kph]", // Use the property getter someBoeing777.Identifier, someBoeing777.SpeedInMilesPerHour, someBoeing777.SpeedInKilometersPerHour); Console.ReadKey(); }
Object initializers are mostly just a convenient syntax for
constructing a new object and then setting some properties.
Consequently, this only works with writable properties—you can’t use it
for immutable types,[11] so this wouldn’t work with our PolarPoint3D
.
We still use the constructor parameter for the read-only Identifier
property; but then we add an extra
section in braces, between the closing parenthesis and the semicolon, in
which we have a list of property assignments, separated by commas. What’s
particularly interesting is that the purpose of the constructor parameter
is normally identifiable only by the
value we happen to assign to it, but the object initializer is “self-documenting”—we can easily see what is
being initialized to which values, at a glance.
The job isn’t quite done yet, though. While there’s nothing
technically wrong with using both the constructor parameter and the object
initializer, it does look a little bit clumsy. It might be easier for our
clients if we allow them to use a default, parameterless constructor, and
then initialize all the members using this new
syntax. As we’ll see in Chapter 6, we have
other ways of enforcing invariants in the object state, and dealing with
incorrect usages. Object initializers are certainly a more expressive
syntax, and on the basis that self-documenting and transparent is better,
we’re going to change how Plane
works
so that we can initialize the whole object with an object
initializer.
As with any design consideration, there is a counter argument. Some classes may be downright difficult to put into a “default” (zero-ish) state that isn’t actively dangerous. We’re also increasing the size of the public API by the changes we’re making—we’re adding a public setter. Here, we’ve decided that the benefits outweigh the disadvantages in this particular case (although it’s really a judgment call; no doubt some developers would disagree).
First, as Example 3-36 shows, we’ll delete
the special constructor from Plane
, and
then make Identifier
an ordinary
read/write property. We can also remove the _identifier
backing field we added earlier,
because we’ve gone back to using an auto property.
Example 3-36. Modifying Plane to work better with object initializers
class Plane { // Remove the constructor that we no longer require // public Plane(string newIdentifier) // { // Identifier = newIdentifier; // } public string Identifier { get; // remove the access modifier // to make it public set; } // ... }
We can now use the object initializer syntax for all the properties we want to set. As Example 3-37 shows, this makes our code look somewhat neater—we only need one style of code to initialize the object.
Example 3-37. Nothing but object initializer syntax
Plane someBoeing777 = new Plane { Identifier = "BA0049", Direction = DirectionOfApproach.Approaching, SpeedInMilesPerHour = 150 };
Object initializer syntax provides one big advantage over offering
lots of specialized constructors: people using your class can provide any
combination of properties they want. They might decide to set the Position
property inline in this object
initializer too, as Example 3-38 does—if
we’d been relying on constructors, default or named arguments wouldn’t
have helped if there was no constructor available that accepted a Position
. We’ve not had to provide an additional
constructor overload to make this possible—developers using our class have
a great deal of flexibility. Of course, this approach only makes sense if
our type is able to work sensibly with default values for the properties
in question. If you absolutely need certain values to be provided on
initialization, you’re better off with constructors.
Example 3-38. Providing an extra property
Plane someBoeing777 = new Plane
{
Identifier = "BA0049",
Direction = DirectionOfApproach.Approaching,
SpeedInMilesPerHour = 150,
Position = new PolarPoint3D(20, 180, 14500)
};
So, we’ve addressed the data part of our Plane
; but the whole point of a class is that it
can encapsulate both state and operations. What
methods are we going to define in our class?