mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
302 lines
9.5 KiB
ReStructuredText
302 lines
9.5 KiB
ReStructuredText
:orphan:
|
||
|
||
.. raw:: html
|
||
|
||
<style>
|
||
table.docutils td, table.docutils th {
|
||
border: 1px solid #aaa;
|
||
}
|
||
</style>
|
||
|
||
|
||
==================================
|
||
Immutability and Read-Only Methods
|
||
==================================
|
||
|
||
:Abstract: Swift programmers can already express the concept of
|
||
read-only properties and subscripts, and can express their
|
||
intention to write on a function parameter. However, the
|
||
model is incomplete, which currently leads to the compiler
|
||
to accept (and silently drop) mutations made by methods of
|
||
these read-only entities. This proposal completes the
|
||
model, and additionally allows the user to declare truly
|
||
immutable data.
|
||
|
||
The Problem
|
||
===========
|
||
|
||
Consider::
|
||
|
||
class Window {
|
||
|
||
var title: String { // title is not writable
|
||
get {
|
||
return somethingComputed()
|
||
}
|
||
}
|
||
}
|
||
|
||
var w = Window()
|
||
w.title += " (parenthesized remark)”
|
||
|
||
What do we do with this? Since ``+=`` has an ``inout`` first
|
||
argument, we detect this situation statically (hopefully one day we’ll
|
||
have a better error message):
|
||
|
||
::
|
||
|
||
<REPL Input>:1:9: error: expression does not type-check
|
||
w.title += " (parenthesized remark)"
|
||
~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
Great. Now what about this? [#append]_ ::
|
||
|
||
w.title.append(" (fool the compiler)")
|
||
|
||
Today, we allow it, but since there’s no way to implement the
|
||
write-back onto ``w.title``, the changes are silently dropped.
|
||
|
||
Unsatisfying Approaches
|
||
=======================
|
||
|
||
We considered three alternatives to the current proposal, none of
|
||
which were considered satisfactory:
|
||
|
||
1. Ban method calls on read-only properties of value type
|
||
2. Ban read-only properties of value type
|
||
3. Status quo: silently drop the effects of some method calls
|
||
|
||
For rationales explaining why these approaches were rejected, please
|
||
refer to earlier versions of this document.
|
||
|
||
Proposed Solution
|
||
=================
|
||
|
||
Terminology
|
||
-----------
|
||
|
||
Classes and generic parameters that conform to a protocol attributed
|
||
``@class_protocol`` are called **reference types**. All other types
|
||
are **value types**.
|
||
|
||
Mutating and Read-Only Methods
|
||
------------------------------
|
||
|
||
A method attributed with ``inout`` is considered **mutating**.
|
||
Otherwise, it is considered **read-only**.
|
||
|
||
.. parsed-literal::
|
||
|
||
struct Number {
|
||
init(x: Int) { name = x.toString() }
|
||
|
||
func getValue() { // read-only method
|
||
return Int(name)
|
||
}
|
||
**mutating** func increment() { // mutating method
|
||
name = (Int(name)+1).toString()
|
||
}
|
||
var name: String
|
||
}
|
||
|
||
The implicit ``self`` parameter of a struct or enum method is semantically an
|
||
``inout`` parameter if and only if the method is attributed with
|
||
``mutating``. Read-only methods do not “write back” onto their target
|
||
objects.
|
||
|
||
A program that applies the ``mutating`` to a method of a
|
||
class—or of a protocol attributed with ``@class_protocol``—is
|
||
ill-formed. [Note: it is logically consistent to think of all methods
|
||
of classes as read-only, even though they may in fact modify instance
|
||
variables, because they never “write back” onto the source reference.]
|
||
|
||
Mutating Operations
|
||
-------------------
|
||
|
||
The following are considered **mutating operations** on an lvalue
|
||
|
||
1. Assignment to the lvalue
|
||
2. Taking its address
|
||
|
||
Remember that the following operations all take an lvalue's address
|
||
implicitly:
|
||
|
||
* passing it to a mutating method::
|
||
|
||
var x = Number(42)
|
||
x.increment() // mutating operation
|
||
|
||
* passing it to a function attributed with ``@assignment``::
|
||
|
||
var y = 31
|
||
y += 3 // mutating operation
|
||
|
||
* assigning to a subscript or property (including an instance
|
||
variable) of a value type::
|
||
|
||
x._i = 3 // mutating operation
|
||
var z: Array<Int> = [1000]
|
||
z[0] = 2 // mutating operation
|
||
|
||
Binding for Rvalues
|
||
-------------------
|
||
|
||
Just as ``var`` declares a name for an lvalue, ``let`` now gives a
|
||
name to an rvalue:
|
||
|
||
.. parsed-literal::
|
||
|
||
var clay = 42
|
||
**let** stone = clay + 100 // stone can now be used as an rvalue
|
||
|
||
The grammar rules for ``let`` are identical to those for ``var``.
|
||
|
||
Properties and Subscripts
|
||
-------------------------
|
||
|
||
A subscript or property access expression is an rvalue if
|
||
|
||
* the property or subscript has no ``set`` clause
|
||
* the target of the property or subscript expression is an rvalue of
|
||
value type
|
||
|
||
For example, consider this extension to our ``Number`` struct:
|
||
|
||
.. parsed-literal::
|
||
|
||
extension Number {
|
||
var readOnlyValue: Int { return getValue() }
|
||
|
||
var writableValue: Int {
|
||
get {
|
||
return getValue()
|
||
}
|
||
**set(x)** {
|
||
name = x.toString()
|
||
}
|
||
}
|
||
|
||
subscript(n: Int) -> String { return name }
|
||
subscript(n: String) -> Int {
|
||
get {
|
||
return 42
|
||
}
|
||
**set(x)** {
|
||
name = x.toString()
|
||
}
|
||
}
|
||
}
|
||
|
||
Also imagine we have a class called ``CNumber`` defined exactly the
|
||
same way as ``Number`` (except that it's a class). Then, the
|
||
following table holds:
|
||
|
||
+----------------------+----------------------------------+------------------------+
|
||
| Declaration:|:: | |
|
||
| | |:: |
|
||
|Expression | var x = Number(42) // this | |
|
||
| | var x = CNumber(42) // or this | let x = Number(42) |
|
||
| | let x = CNumber(42) // or this | |
|
||
+======================+==================================+========================+
|
||
| ``x.readOnlyValue`` |**rvalue** (no ``set`` clause) |**rvalue** (target is an|
|
||
| | |rvalue of value type) |
|
||
| | | |
|
||
+----------------------+ | |
|
||
| ``x[3]`` | | |
|
||
| | | |
|
||
| | | |
|
||
+----------------------+----------------------------------+ |
|
||
| ``x.writeableValue`` |**lvalue** (has ``set`` clause) | |
|
||
| | | |
|
||
+----------------------+ | |
|
||
| ``x["tree"]`` | | |
|
||
| | | |
|
||
+----------------------+----------------------------------+ |
|
||
| ``x.name`` |**lvalue** (instance variables | |
|
||
| |implicitly have a ``set`` | |
|
||
| |clause) | |
|
||
+----------------------+----------------------------------+------------------------+
|
||
|
||
The Big Rule
|
||
-------------
|
||
|
||
.. Error:: A program that applies a mutating operation to an rvalue is ill-formed
|
||
:class: warning
|
||
|
||
For example:
|
||
|
||
.. parsed-literal::
|
||
|
||
clay = 43 // OK; a var is always assignable
|
||
**stone =** clay \* 1000 // **Error:** stone is an rvalue
|
||
|
||
swap(&clay, **&stone**) // **Error:** 'stone' is an rvalue; can't take its address
|
||
|
||
**stone +=** 3 // **Error:** += is declared inout, @assignment and thus
|
||
// implicitly takes the address of 'stone'
|
||
|
||
**let** x = Number(42) // x is an rvalue
|
||
x.getValue() // ok, read-only method
|
||
x.increment() // **Error:** calling mutating method on rvalue
|
||
x.readOnlyValue // ok, read-only property
|
||
x.writableValue // ok, there's no assignment to writableValue
|
||
x.writableValue++ // **Error:** assigning into a property of an immutable value
|
||
|
||
Non-``inout`` Function Parameters are RValues
|
||
----------------------------------------------
|
||
|
||
A function that performs a mutating operation on a parameter is
|
||
ill-formed unless that parameter was marked with ``inout``. A
|
||
method that performs a mutating operation on ``self`` is ill-formed
|
||
unless the method is attributed with ``mutating``:
|
||
|
||
.. parsed-literal::
|
||
|
||
func f(x: Int, inout y: Int) {
|
||
y = x // ok, y is an inout parameter
|
||
x = y // **Error:** function parameter 'x' is immutable
|
||
}
|
||
|
||
Protocols and Constraints
|
||
-------------------------
|
||
|
||
When a protocol declares a property or ``subscript`` requirement, a
|
||
``{ get }`` or ``{ get set }`` clause is always required.
|
||
|
||
.. parsed-literal::
|
||
|
||
protocol Bitset {
|
||
var count: Int { **get** }
|
||
var intValue: Int { **get set** }
|
||
subscript(bitIndex: Int) -> Bool { **get set** }
|
||
}
|
||
|
||
Where a ``{ get set }`` clause appears, the corresponding expression
|
||
on a type that conforms to the protocol must be an lvalue or the
|
||
program is ill-formed:
|
||
|
||
.. parsed-literal::
|
||
|
||
struct BS {
|
||
var count: Int // ok; an lvalue or an rvalue is fine
|
||
|
||
var intValue : Int {
|
||
get {
|
||
return 3
|
||
}
|
||
set { // ok, lvalue required and has a set clause
|
||
ignore(value)
|
||
}
|
||
}
|
||
|
||
subscript(i: Int) -> Bool {
|
||
return true // **Error:** needs a 'set' clause to yield an lvalue
|
||
}
|
||
}
|
||
|
||
-----------------
|
||
|
||
.. [#append] String will acquire an ``append(other: String)`` method as part of the
|
||
formatting plan, but this scenario applies equally to any
|
||
method of a value type
|