- When and how to use Value and Reference Types in Swift
- KHAWER KHALIQ
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: Deepmissea
- Proofread by VernonVan, LeviDing
Value types and reference types in Swift refer to north
In this article, we’ll explore the semantics of value types versus reference types, some of the distinctive features and key benefits of using value types in Swift. Then we’ll focus on when to use value types or reference types when designing programs.
Value types and reference types in Swift
Swift is a multi-paradigm programming language. It has classes, which are the building blocks of object-oriented programming. Classes in Swift can define properties and methods, specify constructors, conform to protocols, and support integration and polymorphism. Swift is also a protocol-oriented programming language that enables abstraction and polymorphism without inheritance through feature-rich protocols and constructs. In Swift, functions are the first type, which can be assigned to variables and passed between functions as arguments and return values. Thus Swift is also suitable for functional programming.
For most object-oriented language developers, the biggest difference in Swift is the rich functionality of the structure. What you can do in a class, other than inheritance, you can do in a structure. This raises the question of when and how to use structures and classes. More generally, the question is when and how to use value types and reference types in Swift.
For completeness, it’s worth noting that value types in Swift aren’t just structures. Enumerations and tuples are also value types. Similarly, reference types are not just classes. Functions are also reference types. Functions, enumerations, and tuples, however, are more specific when used. Swift’s debate about value types and reference types centers on structures and classes. This is the main focus of this article, so the term value types and reference types can be converted to and from term structures and classes.
Now let’s start with some fundamentals, namely the difference between value and reference semantics.
Value and reference
Using value semantics, variables and the data assigned to them are logically unified. Since variables exist on the stack, value types are called stack assignments in Swift. Exactly, all instances of value types are not always on the stack. Some may only exist in CPU registers, others may actually be allocated on the heap. Logically, an instance of a value type can be considered to be contained within the variable to which it is assigned. There is a one-to-one relationship between a variable and a value. Values wrapped in variables cannot be operated on independently of variables.
On the other hand, variables and data are different when reference semantics are used. Instances of reference types are allocated in the heap, and variables contain only a reference to the memory location where the data is stored. It is possible and common for an instance to reference multiple variables. Any of these references can be used to manipulate instances.
This has some impact when assigning a value or reference type instance to a new variable or passing it to a function. Because an instance of a value type can have only one owner, the instance is copied and assigned to a new variable or passed in to a function. Each copy can be modified without affecting each other. For reference types, only the reference is copied, and the new variable or function gets a new reference to the same instance. If you modify an instance of a reference type with any reference, it affects all other reference holders because they all hold references to the same instance.
Let’s look at the code.
struct CatStruct {
var name: String
}
let a = CatStruct(name: "Whiskers")
var b = a
b.name = "Fluffy"
print(a.name) // Whiskers
print(b.name) // Fluffy
Copy the code
We define a structure that represents a cat with a name attribute. We create an instance of CatStruct, assign it to a variable, and then assign that variable to a new variable, changing the name attribute with the new variable. Since structs are value semantics, the act of assigning a value to a new variable causes the instance to be copied, and we end up with two catstructs with different names.
Now, we do the same thing with classes:
class CatClass {
init(name: String) {
self.name = name
}
var name: String
}
let x = CatClass(name: "Whiskers")
let y = x
y.name = "Fluffy"
print(x.name) // Fluffy
print(y.name) // Fluffy
Copy the code
In this case, changing the name attribute with the new variable also changes the name attribute of the first variable. This is because classes are referential semantics, and the act of assigning a value to a new variable does not create a new instance. Both variables hold references to the same instance, leading to implicit data sharing, which can have an impact on how and when you use reference types.
Different concepts of variability
To understand the difference in variability between value types and reference types, we must distinguish variable variability from instance variability.
As we have seen above, instances of value types and assigned variables are logically identical. Therefore, if a variable is immutable, the variable ignores making the instance immutable, regardless of whether the instance has mutable properties or mutable methods. Instance variability takes effect only when an instance of a value type is assigned to a mutable variable.
For reference types, instances and assigned variables are different, and therefore their variability is different. When we declare an immutable variable referencing an instance, we can be sure that the reference to the variable will never change. That is, it always points to the same instance. The mutable properties of an instance can still be changed by this or other references. If a class instance is to be immutable, all of its storage properties must be immutable.
In the previous code, we saw that A can declare the first CatStruct instance as a let constant because it will not be modified. B must be declared as a var because we changed its name attribute and value. For CatClass, x and y are both declared as let constants, whereas we can modify the name attribute.
A characteristic defined as a value type
To better understand when and how value types are used, we need to look at some of the characteristics defined as value types:
- ** Attribute based equality: ** Any two values of the same type whose attributes are equal are considered equal. Consider a
currency
Type, which indicates that the currency has monetary and monetary properties. If we create a $5 instance, it is equal to any other $5 instance. - ** Diluted identity and generation cycle: ** value types have no fixed identity. It is defined only by its attributes. This is the case for simple values like the number 2 or “Swift.” The same is true for complex values. Values also have no lifetime to store state changes. It can be created, destroyed, or rebuilt at any time. It represents $5
currency
Instance, equal to any other instance representing $5, regardless of when or how the two instances were created. - ** fungibility :** The lack of explicit identification and lifecycle gives value types fungibility, which means that if two instances are equal, i.e. they pass attribute-based equality tests, then any instance can be freely replaced. Back to our
currency
Type example. Once we have created an instance representing $5, the program is free to create or discard copies of the instance as the case may be. Whenever we need to submit a $5 instance, it doesn’t matter whether the $5 instance is the one we created earlier, we care about the properties of the value.
Advantages of using value types
Efficiency of 1.
Reference types are allocated on the heap, which is more expensive than allocation on the stack. To ensure that memory is freed when reference types are not needed, a reference count is maintained for all activities for each reference type and instances are destroyed when no reference is available. Value types don’t have this overhead, so they are efficient at creation and replication. Copying of value types is cheap because instances of value types are copied at constant time.
Swift implements built-in extensible data structures such as String, Array, Dictionary, and so on. However, these cannot be allocated on the stack because their size is not known at compile time. To efficiently use heap allocation and preserve value semantics, Swift uses an optimization technique called copy-on-write. This means that each replicated instance is a logical copy, and the actual copy is created on the heap only when the replicated instance changes, until then all logical copies point to the same underlying instance. Better performance is provided because fewer copies are created and, at the time of creation, a fixed number of reference counting operations are involved. This performance tuning can also be used for custom value types if desired.
2. Predictable code
When a reference type is used, no part of the code that holds a reference to an instance can determine what that instance contains, because it can be modified using any other reference. Because instances of value types are copied without implicit data sharing, we do not need to consider the unintended consequences of the behavior of one part of the code affecting the behavior of other parts. Also, when we see a variable declared as a LET constant and holding an instance of a value type, we can be sure that the value cannot be modified no matter how the value type is defined. This provides strong guardianship and fine-grained control over the behavior of the code, making it easy to reason and predict.
One could argue that you could write code such that a copy of a reference type instance is created each time it is handed to a new owner. But this leads to a lot of defensive copying, which can be very inefficient because copying a reference type is expensive. If the reference type instance being copied has properties that are also reference type instances, and we want to avoid any implicit data sharing, then we have to create a deep copy every time, which makes performance worse. We can also try to solve the problem of shared state and variability by making all reference types immutable. But this still involves a lot of inefficient copying, and not being able to change the state of a reference type loses its purpose.
3. Thread safety
Value type instances can be used in multi-threaded environments without worrying that one thread is changing the state of another thread instance. There is no need to implement a synchronization mechanism because there are no race conditions and deadlocks. Writing multithreaded code using value types is simpler, safer, and more efficient.
4. No memory leaks
Swift uses automatic reference counting and, in the absence of references, frees reference type instances. This addresses memory leaks during normal events. However, memory leaks can still occur through strong circular references, when two class instances strongly reference each other and prevent each other from being freed. The same happens when a class and a closure (also a reference type in Swift) strongly reference each other. Since the value types are not referenced, the memory leak problem does not exist.
5. Easy to test
Because reference types retain state for their lifetime, mock frameworks are often used when unit testing reference types to see how various methods are invoked on the state and behavior of the test object. And because the behavior of reference type instances changes from state to state, you often need to set up code to ensure that the test object is in the correct state. For value types, all you care about are the properties of the value type. So all we need to do is create a new value that has the same properties as the expected value properties.
Design programs with value types and reference types
Value types and reference types should not be seen as competing. Their different semantics and behaviors make them suitable for different situations. Our goal is to understand and apply value and reference semantics so that they are combined in a way that best meets the application goals.
1. Use reference types to simulate entities with identities
Almost all real-world domains have entities that maintain identity and state throughout their life cycles. These entities should be modeled using classes.
Consider having a compensation application that uses employee types to represent employees. Simply, assume that only the first and last names of employees are stored. It is possible to have two or more employee instances with the same name, but this does not make them equal, because in the real world these instances represent different employees.
If you assign an employee class instance to a new variable or pass it to a function, the new reference refers to the same instance. That’s what we know for sure. For example, if we use a reference in one module of our application to record an employee’s hours, then when another module is applied to calculate monthly wages, it uses the same instance with the correct hours. Similarly, if we update an employee’s address at a location, then all of our references to the employee will be updated to the correct address, since they are references to the same instance.
Trying to emulate an employee using a structure can lead to errors and inconsistencies, because every time you assign an employee instance to a variable or pass it to a function, it gets copied. Different parts of the program end up with their own instances, and a state change in one part does not show up in the other.
2. Encapsulate state and exposure behavior with value types
Although entities with identities and life cycles need to be modeled with classes, value types are needed to encapsulate their state, represent the associated business, and expose behavior.
Continue with the employee type example. Suppose you want to keep personal data on each employee, salary performance information. We can create personal information, salary, and performance value types that tie together elements of status, business rules, and behavior. This makes the class less bloated, because it only maintains the identity, and it contains instances of value types that handle the various elements and associated behavior of that state.
This is also very consistent with the single principle. For example, the customer code is only interested in the performance of the employee, rather than the employee type having to implement some method to expose various levels of behavior, so it is handled by the performance instance. Because we’re dealing with value types, we don’t have to worry about implicit data sharing and client-side changes that affect the state of the employee instance.
This approach is also more suitable for multithreading. Copies of value type instances of various elements that represent the state of reference type instances can be switched freely to processes on different threads without synchronization. This improves performance and improves the responsiveness of application interactions.
The importance of context
Note that the choice of time-valued and reference types is context-driven. Application development is not an exercise in modeling the real world in an absolute sense, but rather a specific aspect of a modeling problem to satisfy a given use case. Therefore, the decision to use value or reference semantics in the context of an application depends on the role that entities play in related domain issues.
Consider the CatStruct and CatClass types introduced earlier. Which model would we prefer to use to simulate a pet cat? Since the instance will represent a real cat, you should use a class. For example, when we take a cat to the vet for vaccination, we don’t want the vet to vaccinate a copy of the cat, which is what happens if a structure is used. However, if we are designing an application that deals with the eating habits of pet cats, then we should use structures that deal with cats in general, rather than looking for a cat with a specific identity. For such an application, our CatStruct would not have a name attribute, but might have attributes such as type of food consumed, number of services per day, etc.
Not long ago, we used money types as an excellent example of the concept of a value-as-model. In the case of banking, finance or other applications, we only care about the attributes of money, the amount and type of money. But if we are building an application for printing, distributing, and ultimately processing physical money, we need to treat each note as an entity with a unique identity and life cycle.
Similarly, for an application developed for a tire manufacturer, each tire may be an entity with a unique identity and life cycle that is used at the point of sale to track returns, warranty claims, and so on. But for the companies that make cars, they may not want to look at tire properties to keep track of which car uses which tire, even though they can see that the cars they make have unique logos and life cycles.
4. Attribute based equalityThe test of
A value type has no fixed identifier to distinguish whether it is an instance of that type. The only way to compare them is to compare their properties. In fact, the concept of attribute equality is so fundamental in value types that it can serve as a guide for deciding whether a particular type is a value type or a reference type. If two instances of a type cannot be compared using attribute-based equality alone, then we have to deal with the identification of some elements, which usually means that they are reference types, or that they can be distinguished by value and reference semantics.
In practice, this means using the == operator to compare whether any two instances are equal. Therefore, all value types must conform to the Equatable protocol.
5. Combine value types and reference types
As mentioned above, it is desirable to encapsulate properties of reference types as instances of value types for the purpose of encapsulating state, representing business rules, and exposing behavior. These value types can be passed efficiently without worrying about unintended consequences such as thread safety. But should value types hold instances of reference types? This should generally be avoided because using reference-type attributes on value types introduces heap allocation, reference counting, and implicit data sharing, affecting the performance and other benefits of value types. In fact, it can cause value types to lose their property-based equality, diluting their identity and fungibility. Therefore, it is important to follow the rule that value and reference semantics are not combined in a way that compromises the integrity of both.
There are many ways to describe how value types and reference types work in practice. As Andy Matuschak points out in this article: Think of objects as a thin necessary layer on top of a predictably pure value layer. In the references section of Andy’s article is Gary Bernhardt’s talk on a way to build systems using what he calls functional cores and imperative shells. The function core consists of pure values, domain-specific logic, and business rules. It is easy to conclude that this system is conducive to concurrency and easy to test because it is isolated from external dependencies through an imperative shell, thus preserving state and connectivity to user interfaces, persistence mechanisms, networks, and so on.
Swift Standard library and Cocoa framework
Swift’s standard library consists mainly of value types. All of the built-in primitive types and collections are implemented using constructs. The parts that make up the Cocoa framework are mostly made up of classes. The reason classes are needed in some places is because they are a good way to MVC, user interface elements, network connections, file handling, and so on.
But Cocoa has a lot of classes in the Foundation framework that are value types, but they exist as reference types because they’re written in Objective-C. This is where the Swift standard covers, providing the bridging of value types to an increasing number of Objective-C reference types. For more details on the bridge types and interaction between Swift and the Cocoa framework, see this page on the Apple Developer website.
conclusion
Swift provides powerful and efficient value types that make our code more efficient, predictable, and thread-safe. This requires an understanding of the differences between value and reference semantics in order to combine value and reference types in a way that best meets your application’s goals.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.