The Swift team recently sent out an email to the community on the mailing list about some future changes in the direction of memory ownership. As users of the upper API, we probably don’t need to know all the facts behind it, but this email from Apple gives a very comprehensive description of Swift values and object memory management, and explains the context step by step. This article is a great reference if you want to learn more about Swift. I tried to translate the whole text and added some notes of my own. It’s a long article, but if you want to advance to Swift, take the time to read through it (or even read through it several times).

If you don’t have time to read it all and want a quick overview of what’s going on, scroll down to the end for my own quick summary.

The document itself is a proposal for future directions for Swift, so the keywords and implementation details may differ, but that doesn’t affect the idea behind the article. You can find the text of this document in Swift’s repo.

This article is quite long and takes a bit of time to read, but because it is gradual, it won’t be too difficult as long as you settle down. I have added personal notes in some sections, which will add some background and my own thoughts. You can think of it as my personal notes (and jokes) as I read the translation, which will appear in the form of “translator’s notes” in the text. But just a word, only for reference, but also hope to be corrected.

introduce

Adding “ownership” as an important feature to Swift has many benefits for programmers. This document acts as both a “manifesto” and a “meta-proposal” for the nature of ownership. In this article we will state the basic objectives of our ownership-related work and describe the general methods used to achieve these objectives. We also propose a specific set of changes and features, each of which will be discussed separately at a finer granularity in the future. What this document is trying to do is provide a framework at a global level to help understand the contribution of each change.

Question the status quo

The copy-on-write feature of value types widely used in Swift has been a great success. However, there are some drawbacks to this feature:

In Swift, “copy on write” means that value types are copied only before they are changed. Traditionally, value types are copied as they are passed or assigned to other variables, but this incurs a significant and unnecessary performance cost. Written copy will be passed in values and assigned to a variable first checks its reference count, if the reference count to 1 (reference only), that means no other variable holds the value, the copy of the current value can completely avoid, in keep the excellent characteristics of immutability of value type at the same time, guarantee the efficiency. Types such as Array and Dictionary in Swift are value types, but the underlying implementation is a reference type, and both make use of copy-on-write techniques for efficiency.

  • Testing for reference counting and reference uniqueness inevitably leads to additional overhead.

  • In most cases, reference counts determine the performance characteristics, but analyzing and predicting the performance of copy-on-write is complex.

  • At any time a value can be copied, and this copying will “escape” the value from its original scope, resulting in most of the underlying buffer being applied to heap memory. It would be much more efficient to allocate memory on the stack, but this requires that we be able to prevent, or at least identify, the values that are about to escape.

Some lower-level programs have more stringent performance requirements. Often they do not require absolute high performance, but predictable performance characteristics. For example, audio processing is not too much work for a modern processor, and can generally be handled with very high sampling rates. But even the slightest unexpected pause immediately catches the user’s attention.

That said, we may want to have smooth performance characteristics to handle these tasks rather than absolute high performance. Avoid spikes in code performance and keep your program running at a predictable level. As such, even if the absolute performance is poor, there are better solutions to the user experience (such as lowering the bit rate) that may be more important and easier than the overall improvement.

Another very common programming task is optimizing existing code, such as when you’re dealing with a performance bottleneck. Usually we find “hot spots” in execution time or memory usage and fix them in some way. But when these hot spots are caused by implicit value copying, Swift now has few tools to deal with this situation. Programmers may try to fall back on using unsafe Pointers, but this will deprive you of the security and expressiveness of collection types in the standard library.

If you feel that going back to an unsafe pointer is too much, maybe going back to using NSArray or NSDictionary is an option. But it’s important to note that the type in an array or dictionary should also be a subclass of NSObject for a fallback to make sense. Because there are also some implicit bridge conversions between types in Swift and types in Foundation, this performance overhead is often overlooked. However, this compromise is not ideal. You also lose Swift’s type safety and generics features, and you may need to modify existing model types significantly, which is often not worth the loss.

We think we’ll be able to correct these problems by introducing some optional features. We collectively call this set of characteristics ownership.

What is ownership?

Ownership means that a piece of code is responsible for ultimately destroying a value. Ownership system is a set of rules and conventions for managing and transferring ownership.

Any language that has the concept of destruction also has the concept of ownership. In languages such as C and non-Arc Objective-C, ownership is managed by the programmer himself. In other languages, such as some C++, ownership is managed by the language. Even in languages with implicit memory management, there are libraries with a notion of ownership, because there are other programming resources besides memory, and it is important to understand that those code should free those resources.

In addition to memory, other resources may include things like audio unit control, ports, and so on. These resources also need to be allocated and released, and their operation logic in this respect is similar to memory.

Swift already has an ownership system, but it is often “little known” : the system is an implementation detail of the language that programmers have little influence over. The content of our proposal can be summarized as follows:

  • We should add a core rule to the ownership system — the Law of Exclusivity. This principle should prevent simultaneous access to a variable in conflicting ways (for example, passing a variable inout to two different functions). This should be a necessary, non-optional change, but we believe it will not adversely affect the vast majority of programs.

  • We should add features that give programmers some control over the type system. The first is to allow “shared” values to be passed on. This will be an optional change that will consist of a set of annotations and language features that programmers can simply opt out of.

  • We should add a feature that allows programmers to express exclusive ownership. In other words, express that a type cannot be copied implicitly. This will be an optional feature that provides a viable means for experienced programmers who want to control at this level. We do not intend to allow normal Swift programs to work with such types.

With these three changes as pillars, we will take the ownership system of the language from implementation details to a more visible level. These three changes have slightly different priorities, but they are inseparable, for reasons we’ll discuss later. For this evil reason, we bundle all three together and call them collectively “ownership” properties.

For more details

The fundamental problem with Swift’s current ownership system is duplication, and all three changes to ownership attempt to avoid duplication.

In a program, a value can be used in many ways. Our implementation needs to ensure that copies of values exist and are available where they are used. As long as the type of the value is replicable, then we can definitely copy the value to meet our needs. However, for the vast majority of usage scenarios, they don’t actually need to own the copied values themselves. There are cases where you need to do this: a variable does not own its current value, for example; it can only store the value somewhere else, where it is held by something else. But this approach is generally of little practical use. A simple operation, such as reading a value from an instance of a class, requires only that the instance itself be available, not that the reading code itself actually owns that value. Sometimes the difference is obvious, but sometimes it’s hard to tell. For example, the compiler generally has no way of knowing what a function will do to its arguments. The compiler can only fall back to default rules on whether to pass ownership of a value. When the default rule is incorrect, the program will make extra copies at run time. So, what we can do is make certain aspects of programs that are written more explicit, which helps compilers know if they need value ownership.

We want to support non-replicable types, and this approach fits in nicely. For most resource destruction, uniqueness is important: memory can only be reclaimed once, files can only be closed once, and locks can only be released once. Naturally, the owner of a reference to such a resource should be unique, and it should not be able to be copied. Of course, we could artificially allow ownership to be shared — for example, we could add a reference count and destroy resources only when the count goes to zero — but there would be an additional cost to using those resources. Worse, this approach introduces concurrency and re-entrancy problems. If ownership is unique, and the language itself dictates that certain operations on a resource can only occur in the code that owns the resource, then naturally, only one piece of code can perform those operations at a time. Once ownership can be shared, this feature disappears. So it would be interesting to add support for non-replicable types to a language, because it would allow us to manipulate resources in a good and efficient abstract representation. However, to support these types, we need to address all aspects of abstraction completely, such as properly annotating the parameters of a function to indicate whether it requires a transfer of ownership. If the annotations are incorrect, the compiler can’t guarantee that everything will work correctly behind the scenes, just by adding copies.

Proprietary ownership would greatly simplify the problem of resource management, but in practice it would also make the program useless. Clever language design (or increased pressure on compiler developers and language developers) allows you to “share” the rest of the program while remaining unique, but it also introduces a lot of complexity. These complexities and how they correspond are shown later in this article.

To solve any of these problems, we need to solve the problem of non-exclusive access to variables. Swift now allows nested access to the same variable. For example, you can pass the same variable as two different inout arguments, or you can call a method on a variable and then access the same variable in the callback argument that the method receives. Such activities are largely discouraged, but they are not banned. Not only that, but both the compiler and the library have to bow and scrape to make sure the program doesn’t go too far if something goes wrong. For example, an Array must hold its own memory when an in-place element replacement change occurs. If we don’t do this, imagine if we somehow reassigned the original array variable at the time of the change, the chunk of memory will be freed while the elements are still being changed. Similarly, it is generally difficult for the compiler to prove that a value in memory is the same in different places within a function, since it can only assume that any non-transparent function call can overwrite memory. The result is that the compiler can only add copies everywhere, like a persecuted delusional patient, to ensure redundancy. Worse, non-exclusive access greatly limits the usefulness of explicit annotations. For example, a shared parameter is meaningful only if it is guaranteed to be valid throughout the method call. However, only by copying the current value of a variable and passing the copied value can you reliably guarantee that the value can be changed in a reentrant manner. In addition, non-exclusive access makes it impossible to implement certain important patterns, such as “stealing” current values and creating new ones, and it would be a bad thing if other code could grab a variable on the way. The only way to solve this problem is to create a rule that prevents multiple contexts from accessing the same variable at the same time. This is one of our proposals – the principle of exclusivity.

All three goals are closely linked and mutually reinforcing. The principle of exclusivity allows explicit annotations to really optimize code by default and enforce specifications for non-replicable types. Explicit annotations, under the principle of exclusivity, provide more optimization opportunities and allow us to use non-replicable types in functions. Non-replicable types validate that annotation is the preferred option even for replicable types, and they also create more situations in which the principle of exclusivity can be applied directly.

Criteria for success

As mentioned above, the DEVELOPMENT core team would like to introduce ownership as an optional enhancement to Swift. For the most part, programmers should be able to ignore and worry about ownership issues. If this proves to be unsatisfying, we will reject the ownership proposal without imposing this obvious burden on ordinary procedures.

Translator’s note: This is really good news.

The principle of exclusivity introduces new static and dynamic restrictions. We believe that these restrictions affect only a small portion of the code, which we should have documented as potentially inconclusive. There is also a performance penalty when we do dynamic limiting. We hope that the optimization potential will at least “make up” for this loss. We will also provide tools for programmers to bypass these security checks if necessary. We will discuss many of these limitations later in the document.

The core definition

value

Any discussion of ownership systems will be at a lower level of abstraction. We are going to talk about some language implementation issues. In this context, when we refer to the word “value,” we are referring to semantically specific instances of values in the account.

For example, the following Swift code:

Var x = [1,2,3] var y = xCopy the code

People would normally say that x and y have the same value here. Let’s call these values semantic values. But at the implementation level, since the variables x and y can be changed independently, the value of y must be a copy of the value of x. We call this an instance of a value. A value instance can remain the same and move around memory, but copying always results in a new value instance. Throughout the rest of this document, when we use the word “value” loosely, we mean the lower-level representation of a value instance.

The meaning of copying and destroying an instance of a value varies slightly from type to type:

  • We call the types that operate on the byte representation with no extra work trivial. For example, Int and Float are ordinary types, as are structs or enums that contain only ordinary values. Most of what we’ve said about ownership in this article doesn’t apply to values of this type. But the principle of exclusivity still applies.

  • For reference types, a value instance is a reference to an object. Copying this value instance means creating a new reference, which increases the reference count. Destroying the value instance means destroying a reference, which reduces the reference count. Keep decreasing the reference count until, of course, it goes to zero and causes the object to be destroyed. It is important to note, however, that when we talk about copying and destroying values here, we only do operations on reference counts, not copying or destroying the object itself.

  • For write-on-copy types, the value instance contains a reference to an in-memory buffer that works in much the same way as the reference type. Again, we remind you that copying values does not mean copying the contents of the buffer into a new buffer.

The rules used are similar for each type.

In Swift, the distinction between value types and reference types is quite important. The biggest use scenario for Swift today is to work with Cocoa frameworks to create apps, while Cocoa frameworks including Foundation are still a dominant framework for reference types. Value types are so widely used in Swift that you can hardly avoid mixing the two types. Starting with Swift 3, the development team is gradually converting the Foundation framework to value types (such as NSURL to URL, NSData to Data), but underneath they contain a bridge to the original object type. This makes the last case above (copy-on-write types) very common. In addition, we often have reference types as members in our own Swift structs and enums. In this case, copy-on-write is not a straightforward feature and requires additional implementation, otherwise we would have to use it as a reference type, which would be problematic. Be careful when dealing with values that contain reference types.

memory

In general, a value can be held in one of two ways: it may be “temporary,” which means that a particular execution context evaluates the value and treats it as an operand; Or it can be “static” and stored somewhere in memory.

For temporary values, their ownership rules are so straightforward that we don’t have to worry too much about them. Temporary values are the result of expressions that are used in specific places, and values that need to be used only in those places. So it’s clear what language implementations need to do: just finish sending them directly to where they need to be, without forcing them to be copied. The user already knows what’s going to happen, so there’s no real need to improve this part.

So much of our discussion of ownership will revolve around values held in memory. In Swift, there are five closely related concepts for handling memory.

A storage declaration is a syntactic concept that declares how related memory will be handled in the language. Now, storage declarations are introduced through let, var, and subscript. A storage declaration is typed, and it also contains definitions that dictate how the storage is read and written. The default implementation of var or let does nothing more than create a new variable to store the value. However, storage declarations can also be computed, that is, there is no need to say that a variable has a corresponding storage behind it.

Storage reference expression is also a syntax concept. It is an expression that references a storage. This concept is similar to “l-value” in other languages, except that it does not have to be used on the left side of an assignment, since the store can be immutable.

Note: Stored reference expressions will be mentioned several times in this article, so I’ll make sure I understand what a stored reference expression is. An L-value expression refers to an expression that points to a specific storage location. In other languages, l-value should be assignable. In Swift, l-value does not need to be assignable because constant values exist and the storage will not change. For example, the access expression p.x to the coordinates of a point Ponit is a store reference expression that accesses the specific stored x value. If x is declared as var, it is equivalent to “l-value” in other languages. If x is defined as let, it cannot be assigned, but this does not affect the existence of p.x as a stored reference expression. In addition, if the square has an area calculation property var area: Float {retrun side * side}, square.area is not a store reference expression because it is not evaluated as a reference to the store.

A storage reference is a linguistic semantic concept that declares a fully filled reference to a specific storage. In other words, it stores the result of an abstract evaluation of a reference expression, but it does not actually access the storage. If the store is a member, it will basically contain values or references to the store. If the store is a subscript, it will contain the value of the index. For example, a store reference expression such as widgets[I].weight might be abstractly evaluated to the following store reference:

  • attributevar weight: DoubleThe storage
  • The subscriptsubscript(index: Int)The index value19: IntStorage of location
  • The local variablevar widgets: [Widget]The storage

A variable is a semantic concept that refers to a unique place in memory where a value is stored. The variable does not need to be mutable (at least it does not need to be mutable in our documentation). In general, storage declarations are the reason variables are created, but they can also be created dynamically in memory (using UnsafeRawPointer, for example). Variables are always of a particular type and have a lifetime. In programming languages, the lifetime is the time between the time a variable begins to exist and the time it is destroyed.

A memory location is a range of locations in memory that can be marked. In Swift, this is basically an implementation detail concept. Swift does not guarantee that any variable will remain at the same memory address for its lifetime, or even that the variable will be stored at the memory address. But there are times when a variable ¥can be temporarily forced at an unchanging address, such as when passing a variable inout to a withUnsafeMutablePointer.

access

A particular evaluation of a stored reference expression is called an access. There are three ways to access: read, assign, and modify. Assignment and modification are write operations, except that assignment replaces the original value completely without reading it. To modify the value, you need to rely on the old value.

All stored reference expressions can be categorized into one of these three access types, depending on the context in which the expression appears. Note that this categorization is superficial: it relies only on the semantic rules of the current context, without consideration and analysis at the deeper level of the program, and without concern for dynamic behavior. For example, a store reference passed with an inout parameter does not care whether the caller actually uses the current value, whether it writes or simply uses it, and the call is always judged to be a change access.

Evaluation of a storage reference expression can be divided into two phases: first, a storage reference is obtained, and after that, access to the storage reference lasts for a period of time. The two phases are usually sequential, but they can also be executed separately in complex situations, such as when the inout parameter is not the last parameter in the call. The goal of phase separation is to minimize the duration of the access while maintaining the easiest extensibility evaluation rule for Swift from left to right.

If you can accept the differences in the previous five concepts, it is natural to separate the evaluation and use of an expression (access to storage). This makes the mental model more complex, but it can be used in more situations, and the cost is relatively acceptable.

Principle of exclusivity

Having established these concepts, we can briefly address the first part of the proposal, the principle of exclusivity. The principle of exclusivity refers to:

If the evaluation of a store reference expression is a store reference implemented by a variable, the duration of access to that reference should not overlap with any other duration of access to the same variable, unless both accesses are read.

One point is deliberately vague: The principle only states that access “should not” overlap, but it does not specify how to enforce this. This is because we will enforce this mechanism differently for different types of storage. We’ll discuss those mechanisms in the next big section. First, we want to talk about some of the consequences of this rule and the strategies we use to meet it.

The duration of exclusivity

The principle of exclusivity states that access must be exclusive for their duration. The duration is determined by the immediate context that led to the access. In other words, this is a static nature of the program, and we know from the introduction that the security issue before us is a dynamic one. As a rule of thumb, we know that static solutions to dynamic problems tend to work only within conservative boundaries; In a dynamic program, there are bound to be scenarios that fail. So a natural question is how to make a general principle work here.

For example, when we pass storage as an inout parameter, the access will continue throughout the call. This requires the caller to ensure that no other access is made to the store during the call. Isn’t the one-size-fits-all approach a little rough? After all, there may be many places in the function being called that don’t actually use the inout parameter. Perhaps we should be a little more specific about tracking access to inout parameters, which we can do in the function being called, rather than imposing exclusivity on the whole caller. The problem is that the idea is so dynamic that it’s hard to provide an efficient implementation for it.

The caller’s inout rule has one key advantage; The caller has a lot of information about exactly what the storage is being passed. This means that caller rules can usually apply the exclusivity principle in a purely static manner without adding dynamic checks or making suspicious assumptions. For example, if a function calls a mutating method on a local variable (all that mutating actually does is pass self as an inout argument), unless the variable is captured by a escaping closure, Otherwise, the function could easily check each access to a variable and verify that there is no overlap between the calls and the calls to ensure that the principle of exclusivity is met. Not only that, the guarantee can be passed down to the caller, who can use the information to prove that its own access is secure.

Note: We tend to think that the use of inout is rare in practice, which means that you are overlooking the essence of mutating method is the inout parameter call. In the standard library, many of the methods used to change arrays or dictionaries are mutating and conform to this principle.

In contrast, the called party’s rules for Inout do not benefit from information that is discarded as soon as the call occurs. This leads to the common optimization problem we discussed in the introduction section today. For example, suppose the caller loads a value from a parameter and then calls a function that the optimizer cannot infer:

extension Array { mutating func organize(_ predicate: (Element) -> Bool) { let first = self[0] if ! predicate(first) { return } ... // something here uses first } }Copy the code

Under the called party’s rule, the optimizer must copy the value of self[0] into first, because it can only assume that predicate has the potential to change the bound value on self in some way. Under the caller rule, the optimizer can use the values of elements in the array as long as the array is unchanged without copying.

Not only that, but what code could we write if we followed the caller rule in the example above? Higher-order operations like this shouldn’t worry about the caller passing predicate that changes the array in a reentrant way. In the example above, simple implementation choices like using the local variable first instead of repeatedly accessing self[0] become semantically important; And it’s incredibly difficult to maintain that sort of thing. So the Swift library generally forbids such re-entry access. However, because the standard library does not completely prevent programmers from doing this, the implementation must do some extra work at run time to ensure that such code does not cause undefined behavior or cause the entire process to fail. If restrictions are imposed, they only apply to situations that should not occur in well-written code, so there is no loss for most programmers.

Therefore, the proposal proposes a rule for access duration similar to inout for callers, which gives subsequent calls a chance to be optimized with minimal semantic cost.

In other words, the amount of code that needs to be changed is minimal, and the amount of code that gets “ugly” or “complex” is manageable.

Composition of value and reference types

We’ve talked a lot about variables. Readers may be wondering what happens with properties.

In the definition scheme we described above, a property is a storage declaration, and a storage property creates a corresponding variable in its container. Access to this variable obviously follows the principle of exclusivity, but since the attributes are organized together in a container, does this cause some additional restrictions? In particular, should the principle of exclusivity prevent access to the same variable or value through different attributes?

Properties can be divided into three types: – instance properties of value types, – instance properties of reference types, and – static and class properties on any type.

Translator’s note: Attributes in Swift don’t seem particularly obvious compared to Objective-C. Because objective-C, after all, has @property as an explicit way to declare properties, in Swift, “variable declarations” written in concrete types rather than methods automatically become properties.

We propose to always treat reference type and static properties as separate properties, and treat value type properties as independent except for a specific (but important) special case. This can be very restrictive, and we will explain in detail why this proposal is necessary and why we distinguish between different types of attributes. There are three main reasons.

Independence and container

The first reason has to do with containers.

For value types, it is possible to access individual attributes as well as the value as a whole. Obviously, accessing a single attribute conflicts with accessing the value as a whole, because accessing the value as a whole means accessing all the attributes of the value at the same time. For example, let’s say we have a variable p: Point (this variable does not need to be a local variable) that contains three storage attributes x, y, and z. If p and P.x could be changed simultaneously and independently, the principle of exclusivity would have a huge loophole. So we have to enforce the principle of exclusivity here, and we have three options:

(This section will be easier to understand after you read about enforcing the principle of exclusivity.)

The first approach is simply to treat a visit to P.x as a visit to P as well. This cleverly closes the hole, because the exclusivity principle we apply to P naturally precludes conflicting access. But this also rules out simultaneous access to other attributes, because access to any other attribute in P triggers access to P, allowing the exclusivity principle to work.

The other two methods require the relationship to be reversed. We can separate out all the principles of exclusivity for individual storage attributes, rather than exclusivity for overall values: access to p is treated as a way to p.x, p.y, and p.z. Or we can parameterize the way exclusivity applies and record the path of the property being accessed, such as “, “.x “and so on. There are two problems with these approaches, however.

First, we don’t always know all the properties, or if those properties are storage properties; The implementation of a type may be opaque to us, such as a generic or reduced type. Access to a calculated property must be treated as access to the entire value because it needs to pass the variable to a getter or setter, and these access methods can be inout or shared. So it actually conflicts with all the other properties. Using dynamic information can make them work, but it introduces many logging methods into the access methods of value types, which is at variance with the core design goal of value types being inexpensive abstraction tools.

Second, while this pattern can be applied relatively easily to static applications of exclusivity, dynamic applications require a lot of dynamic records, which is also incompatible with our performance goals.

Translator’s note: The attribute access path considered here bears some resemblance to objective-C’S KVC. But that’s exactly what Swift is trying to avoid. Annotation like this, or dynamic modification, is a significant performance penalty, and we want to apply static methods to ensure exclusivity whenever possible. Use dynamic only when it really cannot be determined statically.

So, while there are ways to separate access to different attributes from access to the global value, this would require us to enforce the exclusivity principle for the global value and require both attributes to be stored. Although this is an important special case, it is only a special case. In other cases, we must fall back to the general rule that access to an attribute is also access to the value as a whole.

These considerations do not apply to static properties and properties of reference types. There is no language structure for accessing all properties of a class at the same time in Swift, and saying that all static properties of a type are themselves meaningless, since any module can add static properties to a type at any time.

Specific performance of independent access

The second reason is based on user expectations.

Avoiding overlapping access to different attributes is at best a minor inconvenience. Under the principle of exclusivity, we can at least avoid “surprisingly long access” : calling a variable on a variable can open a long sequence of events that are not obvious, and then go back and modify the original variable. Now with the principle of exclusivity, this would no longer happen because it would result in two conflicting and overlapping accesses to the same variable.

By contrast, many of the patterns that have become customary in reference types rely on this “notification based” approach to update. In fact, in UI code, it is not uncommon for different properties on the same object to be modified in parallel: for example, by some background operation at the same time as the user UI operation. Blocking independent access would break this practice, which is unacceptable.

For static attributes, programmers expect them to be independent global variables; It does not make sense to block access to a global variable while it is being accessed.

Independence and optimizer

The third point has to do with the optimization potential of attributes.

Part of the goal of the exclusivity principle is to make a class of optimizations applicable to values. For example, a non-mutating method on a value type can assume that self will remain exactly the same during a method call. It does not need to worry that some unknown function will return and change the value of self during the call, because such a change would violate the principle of exclusivity. Even in the mutating method, no other code can access self unless the method is told. These assumptions are critical to optimizing Swift code.

However, these assumptions generally do not apply to global variables and content referencing type attributes. Class references can be shared arbitrarily, so the optimizer must assume that some unknown method may access the same instance. In addition, any code in the system (if access control is ignored) can access global variables. So even if access to different attributes is treated as independent, the benefits of language implementation are very limited.

The subscript

Although subscripts are never technically stored properties in the language today, most of the discussion still applies to subscripts. Access to a part of a value type by subscript is treated the same as access to the entire value, and should be considered the same when other access to that value overlaps. The most important result of this is that two different array elements cannot be accessed simultaneously. This interferes with some of the usual ways of manipulating arrays, but some things (such as changing different slices of an array in parallel) are inherently problematic in Swift. We believe that the main impact can be mitigated by purposeful improvements to the collections API.

In daily development, manipulation of arrays is probably one of the more common multithreading problems in parallel programming. For the most part, subscripting is similar to accessing instance properties, and arrays can be kept thread-safe by locking or GCD barriers. If the principle of exclusivity can be solved at the language level, it will greatly reduce the difficulty of parallel program development. This also means that in future libraries we can get thread-safe arrays, or even that the entire library and even third-party code will be thread-safe by default.

Compulsory application of the principle of exclusivity

To apply the principle of exclusivity, we have three possible mechanisms: static enforcement, dynamic enforcement, and undefined enforcement. The mechanism to be chosen must be easily determined by the storage declaration, because the definition of the storage and all direct access methods to it must satisfy the requirements of the declaration. In general, we decide which mechanism applies by storing the type declared as, its container (if any), and any declaration markers that exist (such as var or inout).

Static force

Under statically enforced mechanisms, the compiler checks to see if the exclusivity principle has been violated and gives a compilation error if so. This should be the preferred mechanism because it is safe, reliable and does not incur runtime wear and tear.

This mechanism can only be used when everything is perfectly determined. For example, for value types, the exclusivity principle can apply because it recursively applies to all attributes, guaranteeing exclusive access to the underlying storage. This is not true for general reference types, because there is no way to prove that a reference to a particular object is the only reference to that object. However, if we can support uniquely referenced class types, the exclusivity principle applies statically to their properties.

In some cases, the compiler may implicitly insert a copy operation to preserve source compatibility and avoid errors. This should only be used in mode where source compatibility is required.

The Swift 4 roadmap has been released, mainly introducing some source code incompatible changes in the String section. At the same time, the Swift 4 compiler will support selecting Swift 3 or Swift 4 for compilation at file granularity through specific compilation flags. This is a step up from the current Swift 2.3 and Swift 3 co-existence, but it doesn’t mean Swift 4 doesn’t need to migrate… But it looks like it’s going to be a lot easier than 2 or 3, because at least we can migrate file by file.

Static coercion can be used for:

  • All kinds of nonmutable variables

  • Local variables, unless affected by the use of closures (more on that later)

  • Inout parameters

  • The instance property of the value type

The dynamic force

Under dynamic coercion, the language implementation will maintain a record to determine whether each variable is currently being accessed. If a conflict is found, it triggers a dynamic error. A static error can also be given by the compiler if the compiler detects a conflict that must be found under dynamic coercion.

When recording, two bits are required for each variable, marking it as one of three states: “Not accessed,” “Read,” and “modified.” While multiple reads can work together at the same time, with a little ingenuity, we can avoid fully logging all of our visits by storing the original state at access time.

We should do our best to keep records. This approach requires a reliable detection of situations that are bound to violate the principle of exclusivity. We didn’t ask it to detect the state of race conditions, but the good news is that it usually does, which is a good thing. Records must be able to handle parallel reads correctly and should not, for example, permanently stop in the “read” state. However, it is acceptable to set the record value to an “unread” state during parallel reads, even if the reader is still active. This allows for non-atomic manipulation of records. However, atomic operations must be used when packing the record values of different attributes of a class into a single byte, because parallel access to different variables is allowed within a class.

For readers unfamiliar with Objective-C, atomic and non-atomic operations may be unfamiliar concepts. Atomic operations are calls that are not interrupted by thread scheduling mechanisms. For example, if a setter for the same property is called in a getter that satisfies an atomic operation, the getter will still return the full correct value, but a property for a nonatomic operation does not have this property, so nonatomic operations are much faster. Swift now has no syntax for setting atomic properties, and all properties default to non-atomic operations. If we need attributes to satisfy atomic operations in Swift, we may now need to add/unlock them ourselves. Also note that atomic/non-atomic operations have nothing to do with thread-safe programs; they are just properties set for a read or write operation of a property.

When the compiler detects an “in-instance” access, that is, when no other code is executed during the access, there is no possibility of reentering the same variable. At this point, the compiler can avoid updating the value of the record and simply check that it now has an appropriate value. This is normal for reads, because reads tend to just copy values during access. When the variable is private or internal, the compiler can detect that all possible access is internal, and it can remove all records. We hope this should be a very common situation.

Dynamic coercion can be used for:

  • Closures are used, and local variables if necessary (more on that later)

  • An instance property of type class

  • Static and class attributes

  • The global variable

We need to provide an annotation for dynamic coercion to degrade it in specific properties and classes to use undefined coercion mechanisms. This provides a way to remove this feature when someone feels that the performance cost of dynamic coercion is too high. Leeway is especially important in the early stages after exclusivity is implemented, because we may still be exploring different implementation approaches and may not have found a comprehensive optimization approach.

After that, we can isolate class instances, which allows us to use static coercion on properties of some class instances.

Undefined force

Undefined coercion means that a conflict will not be detected statically or dynamically, and the result of a conflict will be undefined behavior. This is not a good mechanism for generic code in Swift’s “secure by default” design, but it’s the only real option for something like unsafe Pointers.

The Unsafe family continues its role as the dustbin in Swift. For C libraries, if there is no better alternative, we may have to accept the sacrifice of security. However, it is advisable to think twice before actually using the C library in Swift. While the effort to rewrite the C library in Swift is not very flattering, the security features of the code should be weighed against the convenience of using the C library directly. If you choose to use C code, it is recommended to use struct for encapsulation rather than excessively using the Unsafe type for interaction.

Undefined coercion will be used to:

  • Unsafe pointermemoryProperties.

The memory attribute in Swift 1 and 2 was changed to pointee in Swift 3. If the Swift team is going to change it back to memory… I was speechless, too. I hope it’s just a slip of the pen by the original author.

The exclusivity of local variables captured by closures

The static enforcement of the principle of exclusivity depends on our being able to statically know where the use of variables occurs. For local variables, this analysis is usually straightforward, but when a variable is captured by a closure, the flow of control complicates things by making usage difficult to understand. Even non-escaped closures can be reentrant or executed in parallel. For variables captured by closures, we take the following principles:

  • If it is possible for closure C to escape, then any access to V that may be performed after some escape time (including access to C itself) must comply with the principle of dynamic coercion, assuming any variable V is captured by C, unless all access is read access.

  • If the closure C does not escape the function, then all of its uses within the function are known. At each point of use, the closure is either called directly or passed as a parameter to another call. For each non-escape closure of such a call, if any closure contains a write to V for each variable captured by closure C, then all access within the closure must be dynamically enforced, and the closure call is statically enforced as an attempt to write to V. In addition, all access can be statically enforced, and calls to closures are treated as reads to V.

Perhaps these rules will evolve over time. For example, we should be able to make some improvements to the rules for direct calls to closures.

Ownership of the specific tools used

Shared values

A new concept will appear in many discussions in this chapter: shared value. As the name suggests, a shared value is a value that is shared between the current context and another part of the program that owns it. Because multiple parts of the program can use this value at the same time, the value must be read-only for all contexts, including those that own it, to enforce the principle of exclusivity. This concept allows programs to abstract values without having to copy them. This works in much the same way that Inout allows programs to abstract variables.

Note: Programs that abstract values or variables may not be easy to understand. Inout allows the program to assign a value to an argument passed in before the function returns, which is an abstract action: the keyword inout abstracts the operation of passing a variable and reassigning it on return into a modifier. The following shared is similar, except that the object it abstracts from is a value.

(Readers familiar with Rust may find similarities between shared values and Rust’s concept of immutable borrowing.)

Translator’s note: It’s true that most of the Rust team has been poached… I was going to learn Rust this year, but NOW I’m going to learn Swift 4 well… In the last three years, I have been learning a new language every year. They are called Swift 2, Swift 3 and Swift 4.

When the source value of a shared value is a storage reference, the shared value is essentially an immutable reference to the storage. The store is accessed as a read operation for the duration of the shared value, so the dependency, exclusivity principle guarantees that no other access can modify the original variable during the access. Some types of shared values may also be bound to temporary values (such as an R-value). Because temporary values are always owned by the current execution context and are used in only one place, this does not pose additional semantic problems.

Just like normal variables or let bindings, we can use shared values in the bound scope. If ownership is also required where a shared value is to be used, Swift will simply assign the value implicitly – the same as a normal variable or let binding.

Limitations of shared values

This section of the document describes several ways to generate and use shared values. However, our current design does not provide a common, “first-class citizen” mechanism for using shared values. The program cannot return a shared value, build an array of shared values, store shared values as struct fields, etc. These restrictions are similar to those that exist for inout references. In fact, they are so similar that we could even introduce a term to encompass both of them: we called them ephemerals.

Our design does not provide complete support for transients. This is a carefully considered decision based on three main considerations:

  • We need to limit the scope of this proposal to what is realistically achievable in the next few months. We hope this proposal will bring a lot of benefits to the language and its implementation, but the proposal itself is already broad and slightly radical. Full support for transients would add a lot of complexity to the implementation and design, which would obviously cause proposals to go beyond their intended scope. In addition, the remaining language design issues are large, and several existing languages have tried transient as a first-class feature, though their results have not been entirely satisfactory.

  • The type system is a trade-off between complexity and articulation clarity. As long as you make the type system more complex, you can always accept more programs, but that’s not necessarily a good trade-off. In a reference-heavy language like Rust, the underlying life-cycle qualification system adds a lot of complexity to the user model. These complexities can be a real burden for users. And it still inevitably falls back to insecure code from time to time to get around some of the limitations of the ownership system. At this point, introducing scoping to Swift is not a straightforward decision.

  • In Swift, a Rust-like lifecycle (scope) system does not need to be as powerful as in Rust. Swift intentionally provides a language model that allows both type authors and the Swift compiler itself to retain a lot of implementation freedom.

    For example, polymorphic storage in Swift is a little more flexible than Rust. The MutableCollection in Swift implements a subscript method that accesses elements by index, but this method can be implemented in almost any way that satisfies this requirement. If code accesses the subscript, and it happens to do so by accessing underlying memory directly, the access will occur in place. But if subscript is implemented as a computed property in getters and setters, access will take place in a temporary variable, and getters and setters will be called when needed. Because Swift’s access model is highly lexical, it leaves open the possibility of running arbitrary code at the end of the access. Imagine that if we were to implement a loop to add these temporary mutable references to an array, we would need to be able to add arbitrary code to the execution queue at each iteration of the loop so that the function could clean up after the array. This is certainly not a low-loss abstraction! A MutableCollection interface, which is more like Rust under the lifecycle rule, requires that subscript returns a pointer to existing memory; In this way, the subscript does not support the implementation of computation at all.

    Subscript access to Swift is an interesting topic. Unlike the normal copying of value variables, array subscripts are accessed directly in place. But this is done with the help of additional Addressors. In the subscript method of an array, instead of returning the value of the subscript element, a lower-level accessor is returned that retrieves the value of the element. In this way, the copy-on-write optimization applies to array subscript access. Subscript methods that return values directly do not benefit because they still “compute” the access to the subscript, even though the computation itself only returns a single value. The Swift team also has detailed documentation on more address locator issues.

    The same problem applies to simple struct members. Rust’s lifecycle rules state that if you have a pointer to a struct, you can create a pointer to a field in that struct, and the new pointer will have the same lifetime as the original pointer. However, this rule assumes not only that the field is actually stored in memory, but that the field is simply stored, that is, you can point to it with a simple pointer, And this pointer will satisfy the standard Application Binary Interface (ABI) for Pointers to the type to which it points. This means that Rust cannot use many memory layout optimizations, such as packing many Boolean fields into a single byte, or simply reducing the alignment of a field. We are reluctant to introduce such guarantees into Swift.

For these reasons, while we are theoretically interested in further exploring more complex systems that can host more applications of transient values, we do not intend to make proposals for the time being. Because such a system mainly consists of changes to the type system, we don’t worry that this will cause ABI stability problems in the long term. We don’t have to worry about source code incompatibilities. We believe that any enhancement in this regard can be done as an extension and deduction of the features we have proposed.

Local transient binding

In Swift, to abstract the storage, you can only pass the storage as an inout parameter, which is a silly limitation. Programmers who want a local Inout binding can easily circumvent this limitation by importing a closure and calling the closure immediately. This is a simple matter, should not be so difficult to achieve.

Shared values make this limitation even more obvious, and it is interesting to replace a local let value with a local shared value: shared values avoid replication at the expense of preventing other access to the original store. Programmers would not be encouraged to use shared instead of lets throughout their code, especially since the optimizer is usually able to eliminate copying. However, the optimizer does not always remove replication operations, so shared microoptimizations can be useful in certain situations. Also, when dealing with non-replicable types, it may be semantically necessary to remove the formal copy operation.

We propose to remove these restrictions directly:

  inout root = &tree.root

  shared elements = self.queue
Copy the code

The initial assignment of the local transient is required, and it must be a store reference expression. Access to such values continues until the end of the remaining scope.

In other words, make the inout and shared declarations available to the average programmer. However, for the vast majority of top-level application developers, these two declaration keywords are unlikely to be used.

Function parameters

Function parameters are the most important way of abstracting values in a program. Swift now provides three ways to pass parameters:

  • Pass by a value that has ownership. This is a general parameter rule, and we cannot explicitly specify the way to use it.

  • Pass through shared values. This is the rule for the self argument to the nonmutating method; we cannot explicitly specify the use of the method.

  • Pass by reference. This is the rule for the inout argument and the self argument of the mutating method.

Translator’s note: Yes, those nonmutating methods also take self. (Otherwise you won’t be able to use self inside a method!)

We propose that we be allowed to specify the use of non-standard cases:

  • Function arguments can be explicitly specified as owned:

      func append(_ values: owned [Element]) {
        ...
      }
    Copy the code

    This cannot be combined with shared or inout.

    Owned cannot be used together with shared or inout.

    It’s just an explicit representation of the default. We should not expect users to write them out very often unless they are dealing with non-replicable types.

  • The argument to a function can be explicitly specified as shared.

      func ==(left: shared String, right: shared String) -> Bool {
        ...
      }
    Copy the code

    This cannot be combined with owned or inout.

    Shared cannot be used with owned or inout.

    If the function argument is a store reference expression, the store is accessed as a read during the call. Otherwise, the parameter expression is evaluated as r-value, and temporary values are shared in the call. It is important to allow temporary values of function arguments to be shared. Many functions will mark their arguments as shared simply because they don’t actually own them. In fact, the more important case for marking shared is when these parameters are semantically used as references to an existing variable. For example, the argument to the equal operator is marked shared because they need to be able to compare non-replicable values without being previously declared. At the same time, this should not prevent programmers from comparing generic literals.

    Like inout, shared is part of the function type. Unlike inout, however, most function compatibility checks (such as method overwriting checks and function transformation checks) should also succeed if shared and Owned do not match. If a function with owned parameters is converted (or overwritten) to a function with shared parameters, the parameter type must actually be copiable.

  • Methods can be explicitly declared consuming.

      consuming func moveElements(into collection: inout [Element]) {
        ...
      }
    Copy the code

    This causes self to be passed as a owned value, so the consuming cannot be mixed with mutating or nonmutating.

    Within a method, self is still an immutable binding value.

    Note: The term consuming here is actually a more precise subdivision of mutating. If no convention is added, the exclusivity of self is guaranteed to be dynamic when using mutating. This is one of the reasons why mutating in structs is now frowned upon by programmers.

The function result

As we discussed at the beginning of this section, it is not easy to extend Swift’s lexical access model to support returning transients from functions. Implementing this access requires some storage-related code to be executed at the beginning and end of the access. After a function returns, the access actually has no ability to further execute the code.

Of course, we can return a callback containing the transient and wait for the caller to finish using the transient before calling the callback, so we can process the stored code for the transient. However, this alone is not enough, because the called may rely on guarantees made by its caller. For example, a mutating on a struct that wants to return an inout reference to a stored property. To get things right, we not only need to make sure that the method cleans up after the property is accessed, but also that the variables bound to self are always valid. What we really want to do is maintain the current context on the called side and in all valid scopes on the calling side, and simply enter a new nested scope on the calling side, taking the transient as an argument. In programming languages, this is a well-understood situation called co-routine. (You can also think of it as a syntactic sugar for a callback function because of scope limitations, where the return, break, and so on all work as expected.)

In fact, coroutines can be used to solve many transient problems. We will explore this question in the next few chapters.

The concept of coroutines can help simplify thread scheduling problems and is the basis of a good asynchronous programming model.

forcycle

Just as there are three ways to pass parameters, there are three ways to loop over a sequence. Each of these can be expressed with a for loop.

Consuming iterations

The first iteration is our already attribute in Swift: the Consuming iteration, in which each step is represented by a Owned value. This is also the only way we can iterate over arbitrary sequences of values that might be generated on demand. For collections of non-replicable types, this iterative approach allows the collection to be finally structured, while the loop takes ownership of the elements in the collection. Because this approach takes ownership of the values generated by the Sequence and no Sequence can be iterated over more than once, this is a Consuming operation for the Sequence.

We can specify this iteration by explicitly declaring the iteration variable owned:

  for owned employee in company.employees {
    newCompany.employees.append(employee)
  }
Copy the code

This approach should also be used by default when the requirement for non-mutable iteration cannot be met. (This is also necessary to ensure source compatibility.)

The next two methods make sense only for sets, not for arbitrary sequences.

In Swift, a Collection must be a Sequence, but a Sequence is not necessarily a Collection.

Non – mutating iteration

What non-mutating iteration does is simply access each element in the collection and leave them intact. That way, we don’t need to copy these elements; Iterating variables can simply be bound to a shared value. This is nonmutating on the Collection.

We can specify this iteration by explicitly declaring the iteration variable shared:

for shared employee in company.employees { if ! employee.respected { throw CatastrophicHRFailure() } }Copy the code

This behavior is the default behavior when the sequence type satisfies a Collection, because it is a more optimized approach for collections.

for employee in company.employees { if ! employee.respected { throw CatastrophicHRFailure() } }Copy the code

If the sequence operates on a store reference expression, the store will be accessed during the loop. Note that this means that the exclusivity principle implicitly guarantees that the collection will not be modified during iteration. Programs can use the intrinsic function copy on operations to explicitly require iteration to apply to stored copy values.

Translator’s note: We more or less already benefit from Swift’s value nature and exclusivity. For example, with a mutable array, we can iterate over it while modifying its contents:

Var array = [1,2,3] for array {let index = array.index(of: v)! array.remove(at: index) }Copy the code

This is thanks to the duplication of iterated variables, which is unthinkable in many other languages. However, this semantically sound approach can cause some headaches for programmers in practice. It would really be a better choice to use explicit annotations to regulate this writing.

Native functions are those functions that are “embedded” in the language that are implemented by the compiler. We will discuss this in more detail in a later chapter.

Mutating iteration

A mutable iteration will access each element, and it is possible to make changes to the element. The iterated variable is an inout reference to an element. This is a mutating operation to a MutableCollection.

This method must explicitly declare the iterated variable with inout:

  for inout employee in company.employees {
    employee.respected = true
  }
Copy the code

The sequence operation must be a store reference expression. During the duration of the loop, the store will be accessed, as in the previous approach, which prevents overlapping access to the collection. (This rule does not apply, however, if the operation defined by a collection type is a non-mutable operation, as is the case for a collection referencing semantics.)

Expresses mutable and immutable iterations

Both mutable and immutable iterations require the set to create a transient at each step of the iteration. In Swift, we have several ways of expressing this, but the most logical way is to use coroutines. Because coroutines do not discard their execution context when they yield values for the caller, it is common usage for a single coroutine to yield multiple values, which fits well with the basic code pattern for loops. The resulting class of coroutines is often called generators, and this is how iteration is implemented in many major languages. In Swift, to implement this pattern as well, we need to allow generator functions to be defined, such as:

mutating generator iterateMutable() -> inout Element { var i = startIndex, e = endIndex while i ! = e { yield &self[i] self.formIndex(after: &i) } }Copy the code

On the consumer side, the way to implement the for loop with generators is obvious; However, it’s less obvious how to allow generators directly in your code. As mentioned above, there are some interesting restrictions on how coroutines can be used because logically the entire coroutine must run in the scope of the original value access. For generators in general, if a generator function does return a generator object of a certain type, the compiler must ensure that the object does not escape access. This is an important source of complexity.

Generic access methods

Swift now provides fairly crude tools for retrieving attributes and subscripts: basically just get and set methods. For tasks where performance is critical, these tools are far from adequate because they do not support direct access to values, and replication must occur. There are slightly more tools available in the standard library that can provide direct access in very limited circumstances, but they are still weak, and we don’t want to expose them to users for a number of reasons.

Ownership gives us an opportunity to revisit this issue, because GET returns an ownership value, so it cannot be used for non-replicable types. What access methods (getters or setters) really need is the ability to produce a shared value, not just the ability to return the value. Again, one possible way to achieve this is to make access methods use some kind of special coroutine. Unlike generators, this coroutine can occur only once. And there is no need to design a way for programmers to call it, since this coroutine will only be used in access methods.

The idea is that instead of defining get and set, we’ll define read and modify in the store declaration:

  var x: String
  var y: String
  var first: String {
    read {
      if x < y { yield x }
      else { yield y }
    }
    modify {
      if x < y { yield &x }
      else { yield &y }
    }
  }
Copy the code

A storage declaration must define either GET or read (or a storage property), but not both.

To be mutable, the storage declaration must define either set or modify. However, you can choose to define both, in which case set is used as an assignment and modify is used as a change. This can be useful when tuning some complex computational properties, because it allows the change operation to take place without forcing a reassignment of the old value read first. However, it is important to note that the behavior of modify must be consistent with the behavior of get and set.

The inherent function

move

The Swift optimizer generally tries to move values rather than copy them, but forcing the move makes sense. For this reason, we propose the move function. Conceptually, the move function is a top-level function of the Swift library:

  func move<T>(_ value: T) -> T {
    return value
  }
Copy the code

And then, this function has some specific meanings. The function cannot be used indirectly, and the parameter expression must be some form of locally owned store: it can be a let, a var, or an inout binding. Calling move is semantically equivalent to moving the current value out of a parameter variable and returning it in the type specified by the expression. Variables returned will be treated as uninitialized in the final initialization analysis. What happens next to the variable depends on the type of variable:

  • The var variable is simply passed back as uninitialized. It is illegal to use it unless it is assigned a new value or initialized again.

  • The inout binding is similar to var, except that it cannot leave scope uninitialized. In other words, if a program wants to leave a scope with an Inout binding, it must assign a new value to the variable regardless of how it leaves the scope (including when an error is thrown). The security of temporarily leaving inout as an undefined variable is guaranteed by the principle of exclusivity.

  • The let variable cannot be initialized again, so it cannot be used again.

This is a direct complement to the current final initialization analysis, which ensures that a local variable is always initialized before it is used.

copy

Copy is a top-level function in the Swift library:

  func copy<T>(_ value: T) -> T {
    return value
  }
Copy the code

The argument must be a store reference expression. The semantics of the function are the same as above: parameter values are returned. The meaning of this function is as follows:

  • It prevents special conversions in syntax. For example, as we discussed above, if the shared parameter is a storage reference, then the storage is accessed during the call. Programmers can prevent this access by calling copy on the store reference beforehand and forcing the copy operation to complete before the function call.

  • This is necessary for types that prevent implicit copying. We’ll talk more about non-replicable types.

endScope

EndScope is the top-level function in the Swift standard library:

  func endScope<T>(_ value: T) -> () {}
Copy the code

The parameter must be a reference to a local let, var, or a separate (non-parametric, non-cyclic) inout or shared declaration. If the argument is let or var, the variable is destroyed immediately. If the argument is inout or shared, the access is terminated immediately.

The final initialization analysis must ensure that the declaration is not used after this call. An error should be given if the store is a VAR caught by an escape closure.

This is useful for situations where you want to terminate access before the control flow reaches the end of the scope. Similarly, micro-optimizations for destroying values can help.

EnScope guarantees that the value entered at the time of the call is destroyed or that access to it has ended. However, it does not promise that these things actually happened at this point in time: the concrete implementation can still end them earlier.

Lens (Lenses)

Now, all storage reference expressions in Swift are concrete: each component statically corresponds to a storage declaration. In the community, there is a general interest in allowing programs to abstract storage, for example:

  let prop = Widget.weight
Copy the code

Here prop will be an abstract reference to the weight property of type (Widget) -> Double.

Translator’s note: For methods of type, this lensing abstraction always exists – because methods don’t have the memory problem of ownership.

This property is relevant to the ownership model, because the result of a normal function must be a owned value: not shared, nor mutable. This means that the lens abstraction described above can only abstract a read operation, not a write operation, and we can only create this abstraction for a property that can be copied. This also means that code that uses lenses requires more copying than code that uses specific store references.

Imagine if lens abstractions were not simple functions, but values of their respective types. The use of a lens then becomes an abstract store reference expression that accesses static unknown members. This requires the implementation of the language to be able to perform some level of dynamic access. However, accessing unknown properties has almost the same problems as accessing known properties that implement unknown properties; That is, languages already need to do similar things in order to implement generics and restore types.

In general, as long as we have the ownership model, this kind of property fits perfectly into our model.

Type that cannot be copied

Non-replicable types can be useful in many advanced situations. For example, they can be used to efficiently express unique ownership. They can also be used to express values that have some sort of independent identifier such as the atomic type. They can also be used as a formal mechanism to encourage code to work more efficiently with types that are expensive to copy, such as large ones. The unifying theme between them is that we don’t want types to be copied implicitly.

The complexity of dealing with non-replicable types in Swift comes from two main sources:

  • Languages must provide tools to move and share values without forcing copying. We have proposed these tools because they are equally important for optimizing the use of replicable types.

  • Generic systems must be able to express non-replicable types without introducing significant source compatibility issues or forcing everyone to use non-replicable types.

The feature itself would be small if not for these two reasons. Just use the move inherent functions we mentioned above to implicitly use the move instead of the copy, and give diagnostic information when it doesn’t work.

moveonlycontext

Generics can be a real problem, though. In Swift, the most straightforward way to model copiable features is to add a Copyable protocol that makes those types that can be copied follow. In this way, the unrestricted type parameter T cannot be assumed to be a replicable type. However, doing so would be disastrous for source compatibility and usability, and we don’t want programmers to have to worry about non-copiable types the first time they’re introduced to generic code.

Also, we don’t want the type to need to be explicitly declared to support Copyable. Support for replication should be default.

So the logical solution is to maintain the current default assumption that all types are replicable, and then allow context selection to turn that assumption off. We call these contexts moveOnly contexts. All contexts that are lexically nested within a MOVEOnly context are also implicitly moveOnly.

A type can provide moveOnly context:

  moveonly struct Array<Element> {
    // Element and Array<Element> are not assumed to be copyable here
  }
Copy the code

This prevents Copyable assumptions on the type declaration, its generic parameters (if any), and the types they are associated with on the inheritance chain.

Extensions can also provide moveOnly context:

  moveonly extension Array {
    // Element and Array<Element> are not assumed to be copyable here
  }
Copy the code

However, with conditional protocol compliance, the type can be declared conditionally replicable:

  moveonly extension Array: Copyable where Element: Copyable {
    ...
  }
Copy the code

Whether a Copyable satisfies a constraint or directly satisfies a constraint, it is an inherited feature of a type and must be declared in the same module that defines the type. (Or if possible, it should be declared in the same file.)

A non-moveOnly extension of a type reintroduces the assumption of replicability into the type and its generic parameters. The idea is that the types in the standard library can add support for non-replicable elements without breaking the compatibility of existing extensions. If a type does not have any Copyable declaration, adding a non-moveOnly extension to it will cause an error.

We cannot arbitrarily add non-moveOnly extensions to moveOnly defined types. This is obvious, otherwise there would be replication feature conflicts. Adding non-MOVEOnly extensions is fine for types that are not MOVEOnly (because they implicitly support copying by default, or are Copyable), and for cases where Copyable is qualified.

The moveOnly function can also define a moveOnly context:

  extension Array {
    moveonly func report<U>(_ u: U)
  }
Copy the code

This invalidates the replication assumption on any new generic parameters and their inherited association types.

There are a lot of details about moveOnly context that we left out. With regard to this issue, we still need a lot of language implementation experience before we can finally find the right design.

One possibility we are considering is that the MoveOnly context will also cancel the copy-able assumption for values of copy-able types. This provides an important optimization tool for code that requires special attention to copy operations.

Of the non-replicable typedeinit

You can define a deinit method for moveOnly value types that do not comply (and do not conditionally comply) with copyables. Note that deinit must be defined in the main domain of the type, not in the extension.

Deinit will be called to destroy the value when it is no longer needed. This allows non-replicable types to be used to express exclusive ownership of resources. For example, here’s a simple file type that guarantees that the file handle will be closed when the value is destroyed:

moveonly struct File { var descriptor: Int32 init(filename: String) throws { descriptor = Darwin.open(filename, O_RDONLY) // Any abnormal exit in 'init' prevents deinit from being called if Descriptor == -1 {throw... } } deinit { _ = Darwin.close(descriptor) } consuming func close() throws { if Darwin.fsync(descriptor) ! = 0 { throw ... } // This is a Consuming function, so it has ownership of itself. // Anything else will not consume self, so the function will be destroyed by calling deinit on // exit. // And deinit will actually close the file handle via the descriptor. }}Copy the code

Swift’s destruction of a value (and the call to deinit) occurs during the period after a value is last used and before the point in time for formal deconstruction. However, the definition of “use” in this definition has not yet been fully decided.

If the value type is a struct, then self in deinit can only be used to refer to the type’s storage property. The store property of self is treated as a local LET constant and used for final initialization analysis; That is, they belong to the deinit method and can be removed.

If the value type is an enum, self in deinit can only be used as the switch operand. Within the Switch, any associated values used to initialize the corresponding binding have ownership of those values. Such a switch would leave self in an uninitialized state.

An explicit copiable type

Another idea we are exploring within non-replicable types is the idea of declaring a type as not implicitly replicable. For example, a large structure can be formally copied, but copying it unnecessarily can have a disproportionate impact on performance. Such a type should be Copyable, and it should request a copy when copy is called. However, the compiler should give diagnostic information when any implicit replication occurs, just as it does with non-replicable types.

Priority of the implementation

This document lays out a lot of work, which we can summarize as follows:

  • Compulsory exclusivity principle:

    • Static force
    • The dynamic force
    • Dynamically enforced optimization
  • New notes and statements:

    • sharedparameter
    • consumingmethods
    • localsharedinoutThe statement
  • New intrinsic functions and their differences:

    • moveFunctions and their associated effects
    • endScopeFunctions and their associated effects
  • Coroutine characteristics:

    • Generic access methods
    • The generator
  • Non-replicable type

    • Future design work
    • The difference between non-replicable types
    • moveonlycontext

In the next release, the primary goal is ABI stability. The prioritization and analysis of these features must center on their impact on the ABI. After taking this into account, our main thoughts on ABI are as follows:

The exclusivity principle will change the guarantee made on parameters, so it will affect the ABI. We must incorporate this rule into the language before ABI locking, or we will lose the opportunity to change this conservative assumption forever. However, the specific way in which the exclusivity principle is implemented does not affect the ABI unless we intend to do some of the work at run time. Moving some of the work to runtime is not necessary and could be changed in future releases. (It should also be noted that the technical exclusivity principle can have a significant impact on the optimizer, but this should be a general project-process consideration and not affect the ABI.)

Swift ABI stability has been an issue for two years. It appears that ABI stability is still not achieved in Swift 4, meaning that binaries compiled by different Swift compilers are not interchangeable (for example, newer versions of Swift apps cannot call older versions of the Swift framework). Without ABI stability, Swift App would still have to include a copy of the Swift runtime, and we would not have been able to use a binary framework. Almost none of Apple’s current internal apps and frameworks are Swift versions and are largely limited by the stability of the Swift ABI.

The library actively ADAPTS ownership annotations to parameters. Those annotations definitely affect the ABI of these libraries. Library developers need time to adapt, and more importantly, they need some way to verify that the annotations are useful. Unfortunately, the best way to verify this is to implement non-replicable types, which is low on the priority list.

Including general access method needs work, will be “the most common attributes and subscript standard access methods from the get/set/materializeForSet for read/set/change the modify. This has ABI implications for all polymorphic attributes and subscript access, so it must also be done first. However, this change to the ABI can be done without actually introducing coroutine access methods into Swift. The important thing is to make sure that the ABI we are using can meet the requirements of the CFA program in the future.

The work in the generator section may change the core collection protocol. Obviously this affects the ABI. Unlike the generic access method, we absolutely need to implement generators to make the ABI fit our needs.

Non-replicable types and algorithms affect only the ABI of the range in which they are adapted in the standard library. If libraries want to adapt and extend them in a standard collection, it must happen before the ABI is stable.

New local declarations and native functions do not affect the ABI. (And for the most part, the least disruptive tasks are also the easiest.)

It seems that there is a lot of work to be done to fit ownership and non-replicable types into the standard library, but it is important for the availability of non-replicable types. If we can’t create an Array that contains an uncopiable type, that would be very limiting for the language.

Translator’s note: Long for always? Ok, to sum up, this proposal proposes to introduce the following changes in a future Swift release (most likely Swift 4) :

  • Enforce the exclusivity principle, and code that violates this principle will get an error:
    • If a violation of the exclusivity principle can be detected statically, a compilation error is given
    • If a violation of the exclusivity principle is detected dynamically at run time, the code crashes when the violation occurs
    • In the case of unsafe Pointers, the exclusivity principle behavior will be undefined
  • addshared.ownedconsumingThe keyword
    • sharedUsed in arguments or declarations to indicate that value ownership is not taken and to avoid unnecessary value copying.
    • ownedconsumingUsed of variables and functions, respectively, to indicate notsharedMethod is called.
  • Enhanced access methods, ingetsetAdd on the basis ofreadmodifyMethods. Among themreadThe correspondingsharedParameters,modifyThe correspondinginout.
  • The new iteration method can be usedsharedinoutTo specify ownership of iteration variables. As a derivative, we need coroutine way to implement. That is, it is possible to introduce native generators into the language. This is also the basis for further asynchronous programming. For more on this, see myAnother blog post.
  • Introduce a series of proprietary inherent functions such asmove.copyendScope. They offer power users of Swift the possibility of managing their own ownership.
  • For non-replicable types, importmoveonlyTo remove the default copy-ability assumption.

In addition, this declaration is just some basic ideas and discussions. That said, some details, such as the name of the keyword or the specific implementation, may change. However, the direction in which the Swift team wants to clarify the ownership of values should remain the same.

These new tools and methods allow us to clarify ownership, avoid duplication and optimize, but this does not mean that programmers who are Swift end users will have to write in a radically different way. Understanding the ownership changes in Swift, however, can give us insight and insight into the design of the language.

If you can’t get your head around this for the time being, after Swift 4 is released we should have more information and examples from language-oriented “users” to help us make our final decision.