Document directory
- From class to struct
- Data inconsistency
- Constants and atomicity
- Avoid external access to internal types
Overview
This article is the seventh part of Objective C. Through this article, I mainly want to explain to you a problem that we may not notice at ordinary times: creating constant and atomic value types.
From type design to class to struct
If we want to design a type that stores the recipient's address, we call this type address. It should contain the following attributes:
Province
City
Zip code
We need to control the ZIP format (all numbers and 6 digits are required). How should we design it? I think many people will write it like this:
Public Class address {
Private string province;
Private string city;
Private string zip;
Public String province {
Get {return province ;}
Set {province = value ;}
}
Public String city {
Get {return city ;}
Set {city = value ;}
}
Public String zip {
Get {return zip ;}
Set {
Checkzip (value); // verify the format
Zip = value;
}
}
// Check whether the ZIP file is correct
Private void checkzip (string value ){
String Pattern = @ "/d {6 }";
If (! RegEx. ismatch (value, pattern ))
Throw new exception ("Zip is invalid! ");
}
Public override string tostring (){
Return string. Format ("province: {0}, city: {1}, ZIP: {2}", province, city, zip );
}
}
The first problem already exists: When we declare a class, we define a series of related operations (or actions and methods). Of course, the class also contains fields and attributes, however, these fields are usually used by the class method, and attributes are often used to indicate the status of the class (such as the length of stringbuilder), the ability of the class (such as the capacity of stringbuilder ), the status or stage of the method. When defining a structure, we usually only use it to store data, instead of providing methods, or simply providing methods to operate or convert itself, instead of providing services for other types.
Address does not contain any method. It only organizes three data, namely Provice, city, and zip, into an independent individual, so it is best to declare it as a struct rather than a class. (This is also an exception: If the address contains 20 or more fields, consider declaring it as a class because the class is passed as a reference when passing parameters, struct is used to transmit values. When the data size is small, the data transmission efficiency is higher. When the data size is large, the data transmission and reference occupy less memory space .)
So we can first declare the address as a struct instead of a class.
Data inconsistency
Next we will use the address type we just created:
Address a = new address ();
A. Province = "Shaanxi ";
A. City = "Xi'an ";
A. Zip = "710068 ";
Console. writeline (A. tostring (); // Province: Shaanxi, city: Xi'an, ZIP: 710068
It seems that there is no problem, but in retrospect, the type definition may throw an exception when assigning values to the zip attribute, so we should put it in a try catch statement, at the same time, we will assign an error value to zip to see what will happen:
Try {
A. City = "Qingdao ";
A. Zip = "12345"; // an exception is triggered here
A. Province = "Shandong ";
} Catch {
}
Console. writeline (A. tostring (); // Province: Shaanxi, city: Qingdao, ZIP: 710068
The result is a data inconsistency problem. When assigning a value to zip, the assignment to zip and the subsequent province fails because of an exception, however, the city value is successfully assigned. The result is that Provice is located in Shaanxi, while city is located in Qingdao.
That is, when the zip value is assigned, no exception is thrown, and a problem occurs: in the case of multithreading, when the current thread executes to change the city to "Qingdao ", however, when zip and province are not modified yet (Zip is still "710068" and province is still "Shaanxi "). If other threads access instance a at this time, the inconsistent data will also be read.
Constants and atomicity
Now that we know the problem above, how can we improve it? Let's take a look at the author's definition of constants and atomicity:
- Atomicity of Objects: The State of the object is a whole. If a field changes, other fields must be changed at the same time. Simply put, it is either not modified or all changed.
- Object Constants: Once the object status is determined, it cannot be changed again. If you want to change it again, you need to reconstruct an object.
We already know the atomic and constant concepts of objects. How can we implement them next? For atomicity, we add a constructor to assign values to all fields of the object in this constructor. To implement constants, we cannot modify the object state after assigning values to the object. Therefore, we delete the set accessors In the attribute and declare the fields as readonly:
Public struct address {
Private readonly string province;
Private readonly string city;
Private readonly string zip;
Public Address (string province, string city, string zip ){
This. City = city;
This. Province = province;
This.zip = zip;
Checkzip (ZIP); // verify the format
}
Public String province {
Get {return province ;}
}
Public String city {
Get {return city ;}
}
Public String zip {
Get {return zip ;}
}
// Others...
}
In this way, we create an address object and assign values to all fields in the constructor as a whole. When we need to change the value of a single field, you also need to recreate the object and assign a value. Let's take a look at the following test:
Address a = new address ("Shaanxi", "Xi'an", "710068 ");
Try {
A = new address ("Qingdao", "Shandong", "22233"); // if an exception occurs, the assignment to a fails, but the status remains the same
} Catch {
}
Console. writeline (A. tostring (); // output: Province: Shaanxi, city: Xi'an, ZIP: 710068
Avoid external access to internal types
The above method solves the data inconsistency problem, but it also misses a point: when the type maintains a reference type field, such as an array. Although we declare it as readonly, it can still be accessed outside the type (if you do not know the difference between the value type and the reference type, referC # type Basics). Now let's modify the Address class and add an array of phones to store the phone number:
Private readonly string [] phones;
Public Address (string province, string city, string zip, string [] phones ){
// Omitted...
This. Phones = phones;
}
Public String [] phones {
Get {return phones ;}
}
Let's perform a test:
String [] phones = {"029-88401100", "029-88500321 "};
Address a = new address ("Shaanxi", "Xi'an", "710068", phones );
Console. writeline (A. Phones [0]); // output: 029-88401100
String [] B = A. Phones;
B [0] = "029-xxxxxxxx"; // The address content is modified through B.
Console. writeline (A. Phones [0]); // output: 029-xxxxxxxx
Although the phones field is declared as readonly, only the get attribute accessors are provided. We can still modify the internal content of object A through the variable B outside address object. How can this problem be avoided? We can solve this problem through deep replication. Add the following code to the get attribute accessors of phones:
Public String [] phones {
Get {
String [] RTN = new string [phones. Length];
Phones. copyto (RTN, 0 );
Return RTN;
}
}
In the get accessors, we create a new array, copy the array content of the address object, and return it to the caller. At this point, run the code again. Because B points to the newly created array object instead of the array object in address object A, the modification to B will not affect. Run the code again and we can get the output from 029-88401100.
But the problem is not over yet. Let's look at the following code:
String [] phones = {"029-88401100", "029-88500321 "};
Address a = new address ("Shaanxi", "Xi'an", "710068", phones );
Console. writeline (A. Phones [0]); // output: 029-88401100
Phones [0] = "029-xxxxxxxx"; // modify the internal data of the address object through the phones variable
Console. writeline (A. Phones [0]); // output: 029-xxxxxxxx
After creating the address object, we can still use the previous array variables to modify the internal data of the object, it is easy to think that we can perform a deep copy of the externally passed array in the constructor:
Public Address (string province, string city, string zip, string [] phones ){
// Omitted above...
This. Phones = new string [phones. Length];
Phones. copyto (this. Phones, 0 );
Checkzip (ZIP); // verify the format
}
In this way, we run the above Code again, and the changes to phones will no longer affect the address object itself.
Summary
This article describes three issues that need to be paid attention to during type design: 1. When the purpose of creating a type is to store a group of related data and the data volume is not large, declaring it as struct is more efficient than class; 2. Declaring the type as atomic and constant can avoid possible data inconsistency issues; 3. In-depth copying of object fields in constructors and get accessors can avoid the problem of modifying internal data of the type externally.