Constants are easy to create. They remain unchanged after being created. If the validity of the parameter is verified during construction, we can ensure that it is in a valid state from then on. Because it is impossible for us to change its internal status. By disabling changing the object state after building an object, we can actually save many necessary error checks. Constants are also thread-safe: Multiple readers can access the same content. If the internal status cannot be changed, different threads will not be able to obtain different values of the same data. Constant types can also be safely exposed to the outside world, because the caller cannot change the internal state of the object. The constant type also performs well in the hash-based set, because the object. the value returned by the gethashcode () method must be a constant (see clause 10), which is obviously guaranteed by a constant type.
However, not all types can be constant types. In that case, we will need to clone the object to change the state of the program. That is why these terms are both for constant and atomic value types. We should break down our types into various structures that can naturally form a single entity. For example, the address type is like this. An address object is a single entity composed of multiple related fields. Changing one field may mean changing other fields. The customer type is not atomic. A customer type may contain many information: address, name, and one or more telephone numbers ). Any of these independent information may be changed. A customer object may need to change its phone number, but it does not need to change its address. It may also change its address, but still retains the same phone number. It may also change its name, but keep the same phone number and address. Therefore, the customer object is not atomic. However, it consists of different atomic types: An address, a name, or a group of phone numbers/type pairs [13]. Atomic types are single entities: We usually Replace the entire content of an atomic type directly. But sometimes there are exceptions, such as changing the fields that constitute it.
The following is a typical implementation of a variable type address:
// Variable structure address.
Public struct address
{
Private string _ line1;
Private string _ line2;
Private string _ city;
Private string _ state;
Private int _ zipcode;
// Depends on the default constructor generated by the system.
Public String line1
{
Get {return _ line1 ;}
Set {_ line1 = value ;}
}
Public String line2
{
Get {return _ line2 ;}
Set {_ line2 = value ;}
}
Public String City
{
Get {return _ city ;}
Set {_ city = value ;}
}
Public String state
{
Get {return _ state ;}
Set
{
Validatestate (value );
_ State = value;
}
}
Public int zipcode
{
Get {return _ zipcode ;}
Set
{
Validatezip (value );
_ Zipcode = value;
}
}
// Ignore other details.
}
// Application example:
Address a1 = new address ();
A1.line1 = "111 s. Main ";
A1.city = "anytown ";
A1.state = "Il ";
A1.zipcode = 61111;
// Change:
A1.city = "Ann Arbor"; // zipcode and state are invalid.
A1.zipcode = 48103; // The State is still invalid.
A1.state = "mi"; // The entire object is normal now.
Changing the internal state means that the object's invariant may be violated-at least temporarily. After we change the city field, A1 is in an invalid state. After the city is changed, it no longer matches the state or zipcode. The above Code seems to be okay, but suppose this code is part of a multi-threaded program, any context switch during the city change process may result in another thread seeing inconsistent data views.
Even if we are not writing multi-threaded applications, the above Code still has problems. If the zipcode value is invalid, an exception is thrown. At this time, we actually only made some changes, and the object will be in an invalid state. To solve this problem, we need to add a considerable amount of internal verification code in the address structure. This will undoubtedly increase the size and complexity of the Code. To fully implement exception security, we also need to put defensive code at all the code blocks that change multiple fields. Thread security also requires that we add thread synchronization checks on each attribute accessor (get and set. All in all, this will be a considerable job-and we have to consider the addition of functionality and possible code extensions over time.
Instead, let's implement the address structure as a constant type. First, you must change all instance fields to read-only fields:
Public struct address
{
Private readonly string _ line1;
Private readonly string _ line2;
Private readonly string _ city;
Private readonly string _ state;
Private readonly int _ zipcode;
// Ignore other details.
}
Delete all set accessors for each attribute at the same time:
Public struct address
{
//...
Public String line1
{
Get {return _ line1 ;}
}
Public String line2
{
Get {return _ line2 ;}
}
Public String City
{
Get {return _ city ;}
}
Public String state
{
Get {return _ state ;}
}
Public int zipcode
{
Get {return _ zipcode ;}
}
}
Now we get a constant type. To make it available, we also need to add the necessary constructor to thoroughly initialize the address structure. Currently, the address structure only requires a constructor to assign values to each of its fields. It is unnecessary to copy the constructor, because the default value assignment operator of C # is efficient enough. Remember, the default constructor is still valid. All strings in the address object created using the default constructor will be null, and zipcode will be 0:
Public struct address
{
Private readonly string _ line1;
Private readonly string _ line2;
Private readonly string _ city;
Private readonly string _ state;
Private readonly int _ zipcode;
Public Address (string line1,
String line2,
String city,
String state,
Int zipcode)
{
_ Line1 = line1;
_ Line2 = line2;
_ City = city;
_ State = State;
_ Zipcode = zipcode;
Validatestate (State );
Validatezip (zipcode );
}
// Ignore other details.
}
To change the constant type, we need to create a new object instead of modifying the existing instance:
// Create an address:
Address a1 = new address ("111 s. Main ",
"", "Anytown", "Il", 61111 );
// Use the reinitialization method to change the object:
A1 = new address (a1.line1,
A1.line2, "Ann Arbor", "mi", 48103 );
Currently, A1 can only be in one of the following States: the original location in anytown, or the new location in Ann Arbor. We will no longer be able to change an existing address object to any invalid temporary state as in the previous example. Invalid intermediate states may only exist in the execution process of the address constructor and cannot appear out of the constructor. Once an address object is constructed, its value remains unchanged. The new address version is also quite secure: A1 is the original value, or a newly constructed value. If an exception is thrown during the construction of the new address object, A1 retains the original value.
For the constant type, we also need to ensure that no vulnerability will cause its internal status to be changed. Because the value type does not support the derived type, we do not have to worry that the derived type will change its field. But we need to pay attention to the variable reference type field in the constant type. When we implement the constructor for such a type, we need to make defensive copies of the variable types. The following example assumes that phone is a constant value type, because we only care about the constants of the Value Type:
// The following types indicate that the status changes leave a vulnerability.
Public struct phonelist
{
Private readonly phone [] _ phones;
Public phonelist (phone [] pH)
{
_ Phones = Ph;
}
Public ienumerator phones
{
Get
{
Return _ phones. getenumerator ();
}
}
}
Phone [] phones = new phone [10];
// Initialize phones
Phonelist PL = new phonelist (Phones );
// Change the phones array:
// It also changes the internal state of the constant type.
Phones [5] = phone. generatephonenumber ();
We know that arrays are a reference type. This means that the array referenced inside the phonelist structure and the external phones array reference the same memory space. In this way, the developer may modify the phonelist of the constant structure by modifying phones. To avoid this possibility, we need to make a defensive copy of the array. The preceding example shows a vulnerability that may exist in a variable set type. If phone is a variable reference type, it is more harmful. In this case, the value in the set may be changed even if the set type can be avoided. At this time, we need to make a defensive copy of this type in all constructors -- in fact, as long as there is any variable reference type in the constant type, we need to do this:
// Constant type: copy the variable reference type during construction.
Public struct phonelist
{
Private readonly phone [] _ phones;
Public phonelist (phone [] pH)
{
_ Phones = new phone [Ph. Length];
// Because phone is a value type, you can directly copy the value.
Ph. copyto (_ phones, 0 );
}
Public ienumerator phones
{
Get
{
Return _ phones. getenumerator ();
}
}
}
Phone [] phones = new phone [10];
// Initialize phones
Phonelist PL = new phonelist (Phones );
// Change the phones array:
// The copy in PL will not be changed.
Phones [5] = phone. generatephonenumber ();
To return a variable reference type, we must follow the same rule. For example, if we want to add an attribute to obtain the entire array from the phonelist structure, the accessors also need to create a defensive copy. For more details, refer to clause 23.
There are usually three policies for initializing constants. Which of the following policies depends on the complexity of a type. Defining an appropriate constructor is usually the simplest policy. For example, the above address structure is to define a constructor for initialization.
We can also create a factory method for initialization. This method is convenient for creating some common values .. Net Framework uses this policy to initialize the system color. For example, the static methods color. fromknowncolor () and color. fromname () can return a corresponding color value based on a specified system color name.
Finally, we can create a variable helper class to construct a constant type only when multiple steps are required .. The string class in net adopts this policy, and its auxiliary class is system. Text. stringbuilder. We can use the stringbuilder class to create a String object through multiple steps. After performing all the necessary operations, we can use the stringbuilder class to obtain the expected String object.
Constant types make our code easier to write and maintain. We should not blindly create get and set accessors for every attribute of the type. To store data types, we should try to implement them as constant and atomic value types. Based on these types, we can easily build more complex structures.