A struct is a simple user-defined type, a lightweight alternative to a class. Structs are similar to classes in that they may contain constructors, properties, methods, fields, operators, nested types, and indexers (see Chapter 9).
There are also significant differences between classes and structs. For instance, structs don’t support inheritance or destructors. More important, although a class is a reference type, a struct is a value type. (See Chapter 3 for more information about classes and types.) Thus, structs are useful for representing objects that don’t require reference semantics.
The consensus view is that you ought to use structs only for types that are small, simple, and similar in their behavior and characteristics to built-in types.
C++ programmers take note: the meaning of C#’s struct construct is very different from C++’s. In C++ a struct is exactly like a class, except that the visibility (public versus private) is different by default. In C#, structs are value types, while classes are reference types, and C# structs have other limitations as described in this chapter.
Structs are somewhat more efficient in their use of memory in arrays (see Chapter 9). However, they can be less efficient when used in some collections. Collections that take objects expect references, and structs must be boxed. There is overhead in boxing and unboxing, and classes might be more efficient in some large collections.
In this chapter, you will learn how to define and work with structs, and how to use constructors to initialize their values.
The syntax for declaring a struct is almost identical to that for a class:
[attributes
] [access-modifiers
] structidentifier
[:interface-list
] {struct-members
}
Example 7-1 illustrates the definition of a struct.
Location
represents a point on a two-dimensional
surface. Notice that the struct Location
is
declared exactly as a class would be, except for the use of the
keyword struct
. Also notice that the
Location
constructor takes two integers and
assigns their value to the instance members, xVal
and yVal
. The x
and
y
coordinates of Location
are
declared as properties.
Example 7-1. Creating a struct
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace CreatingAStruct { public structLocation { private int xVal; private int yVal; public Location( int xCoordinate, int yCoordinate ) { xVal = xCoordinate; yVal = yCoordinate; } public int x { get { return xVal; } set { xVal = value; } } public int y { get { return yVal; } set { yVal = value; } } public override string ToString( ) { return ( String.Format( "{0}, {1}", xVal, yVal ) ); } } public class Tester { public void myFunc( Location loc ) { loc.x = 50; loc.y = 100; Console.WriteLine( "In MyFunc loc: {0}", loc ); } static void Main( ) { Location loc1 = new Location( 200, 300 ); Console.WriteLine( "Loc1 location: {0}", loc1 ); Tester t = new Tester( ); t.myFunc( loc1 ); Console.WriteLine( "Loc1 location: {0}", loc1 ); } } } Output: Loc1 location: 200, 300 In MyFunc loc: 50, 100 Loc1 location: 200, 300
Unlike classes,
structs don’t support
inheritance. They implicitly derive from object
(as do all
types
in C#, including the built-in types) but can’t
inherit from any other class or struct. Structs are also implicitly
sealed (that is, no class or struct can derive
from a struct). Like classes, however, structs can implement multiple
interfaces.
Additional differences
include the following.
Structs can’t have destructors, nor can they have a custom parameterless (default) constructor. If you don’t have a constructor, the CLR will initialize your structure and zero out all the fields. If you do provide a nondefault constructor, the CLR initialization will not occur, and so you must initialize all the fields explicitly.
You can’t initialize an instance field in a struct. Thus, it is illegal to write:
private int xVal = 50; private int yVal = 100;
though that would have been fine had this been a class.
Structs are designed to be simple and lightweight. While private
member data promotes data-hiding and encapsulation, some programmers
feel it is overkill for structs. They make the member data public,
thus simplifying the implementation of the struct. Other programmers
feel that properties provide a clean and simple interface, and that
good programming practice demands data-hiding even with simple
lightweight objects. With the new refactoring ability in Visual
Studio, it’s easy to turn your previously public
variables into private variables with associated public properties.
Just right-click on the variable, and choose
Refactor→ Encapsulate
Field. Visual Studio will change your public variable to private and
create a property with get
and
set
accessors.
Create
an instance of a struct by using the
new
keyword in an assignment statement, just as you would for a class. In
Example 7-1, the Tester
class
creates an instance of Location
as follows:
Location loc1 = new Location(200,300);
Here the new instance is named loc1
and is passed
two values, 200
and 300
.
The
definition of the Tester
class in Example 7-1 includes a Location
object[1] struct
(loc1
) created with the values
200
and 300
. This line of code
calls the Location
constructor:
Location loc1 = new Location(200,300);
Then WriteLine( )
is called:
Console.WriteLine("Loc1 location: {0}", loc1);
WriteLine()
is expecting an object, but, of
course, Location
is a struct (a value type). The
compiler automatically boxes the
struct (as it would any value type), and it is the boxed object that
is passed to WriteLine( )
.
ToString()
is called on the boxed object, and
because the struct (implicitly) inherits from
object
, it is able to respond polymorphically,
overriding the method just as any other object might:
Loc1 location: 200, 300
Console.WriteLine("Loc1 location: {0}", loc1.ToString());
You avoid the box operation by calling ToString
directly on a variable of a value type where the value type provides
an override of ToString
.
Structs are value objects, however, and when passed to a function,
they are passed by value—as seen in the next line of code, in
which the loc1
object is passed to the
myFunc()
method:
t.myFunc(loc1);
In myFunc()
, new values are assigned to
x
and y
, and these new values
are printed out:
Loc1 location: 50, 100
When you return to the calling function (Main()
)
and call WriteLine( )
again, the values are
unchanged:
Loc1 location: 200, 300
The struct was passed as a value object, and a copy was made in
myFunc( )
. Try changing the declaration to
class
:
public class Location
and run the test again. Here is the output:
Loc1 location: 200, 300 In MyFunc loc: 50, 100 Loc1 location: 50, 100
This time the Location
object has reference
semantics. Thus, when the values are changed in
myFunc( )
, they are changed on the actual object
back in Main()
.[2]
Because loc1
is a
struct (not a class), it is created on the stack. Thus, in Example 7-1, when the new
operator is
called:
Location loc1 = new Location(200,300);
the resulting Location
object is created on the
stack.
The new
operator calls the
Location
constructor. However, unlike with a
class, it is possible to create a struct without using
new
at all. This is consistent with how built-in
type variables (such as int
) are defined, and is
illustrated in Example 7-2.
A caveat: I am demonstrating how to create a struct without using
new
because it differentiates C# from C++ and also
differentiates how C# treats classes versus structs. That said,
however, creating structs without the keyword new
brings little advantage and can create programs that are harder to
understand, more error-prone, and more difficult to maintain. Proceed
at your own risk.
Example 7-2. Creating a struct without using new
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace StructWithoutNew { public structLocation { public int xVal; public int yVal; public Location( int xCoordinate, int yCoordinate ) { xVal = xCoordinate; yVal = yCoordinate; } public int x { get { return xVal; } set { xVal = value; } } public int y { get { return yVal; } set { yVal = value; } } public override string ToString( ) { return ( String.Format( "{0}, {1}", xVal, yVal ) ); } } public class Tester { static void Main( ) { Location loc1; // no call to the constructor loc1.xVal = 75; // initialize the members loc1.yVal = 225; Console.WriteLine( loc1 ); } } }
In Example 7-2, you initialize the local variables
directly, before calling a method of loc1
and
before passing the object to WriteLine()
:
loc1.xVal = 75; loc1.yVal = 225;
If you were to comment out one of the assignments and recompile:
static void Main() { Location loc1; loc1.xVal = 75; // loc1.yVal = 225; Console.WriteLine(loc1); }
you would get a compiler error:
Use of unassigned local variable 'loc1'
Once you assign all the values, you can access the values through the
properties x
and y
:
static void Main() { Location loc1; loc1.xVal = 75; // assign member variable loc1.yVal = 225; // assign member variable loc1.x = 300; // use property loc1.y = 400; // use property Console.WriteLine(loc1); }
Be careful when using properties. Although they allow you to support encapsulation by making the actual values private, the properties themselves are actually member methods, and you can’t call a member method until you initialize all the member variables.
[1] Throughout this book, I use the term
object
to refer both to
reference
types and to value types. There is some debate in the object-oriented
world about this, but I take solace in the fact that Microsoft has
implemented the value types as
if they inherited from the root class
Object
(and thus you may call all of
Object
’s methods on any value
type, including the built-in types such as
int
).
[2] Another way to
solve this problem is to use the keyword ref
(as
explained in the Section 4.5.1
section in Chapter 4), which allows you to pass
a value type by reference.
18.116.15.161