rfc:readonly_properties_v2
Differences
This shows you the differences between two versions of the page.
Next revision | Previous revisionNext revisionBoth sides next revision | ||
rfc:readonly_properties_v2 [2021/06/02 15:13] – created nikic | rfc:readonly_properties_v2 [2021/07/01 12:10] – Fix a bunch of typos theodorejb | ||
---|---|---|---|
Line 2: | Line 2: | ||
* Date: 2021-06-02 | * Date: 2021-06-02 | ||
* Author: Nikita Popov < | * Author: Nikita Popov < | ||
- | * Status: | + | * Status: |
* Target Version: PHP 8.1 | * Target Version: PHP 8.1 | ||
* Implementation: | * Implementation: | ||
Line 8: | Line 8: | ||
===== Introduction ===== | ===== Introduction ===== | ||
- | This RFC introduces a '' | + | This RFC introduces a '' |
- | Value objects are often immutable: Properties are initialized once in the constructor, | + | Value objects are often immutable: Properties are initialized once in the constructor, |
<PHP> | <PHP> | ||
Line 24: | Line 24: | ||
</ | </ | ||
- | This doesn' | + | This doesn' |
- | Unfortunately, | + | Support for first-class readonly properties allows you to directly expose |
<PHP> | <PHP> | ||
Line 76: | Line 76: | ||
<PHP> | <PHP> | ||
class Test { | class Test { | ||
- | public readonly int $prop = 0; | + | |
+ | | ||
+ | public readonly array $ary = [], | ||
+ | ) {} | ||
} | } | ||
$test = new Test; | $test = new Test; | ||
- | $test->prop += 1; | + | $test->i += 1; |
- | $test->prop++; | + | $test->i++; |
- | ++$test-> | + | ++$test-> |
- | $ref =& $test->prop; | + | $test-> |
- | $test->prop =& $ref; | + | $test-> |
- | byRef($test-> | + | $ref =& $test->i; |
+ | $test->i =& $ref; | ||
+ | byRef($test-> | ||
foreach ($test as & | foreach ($test as & | ||
</ | </ | ||
Line 115: | Line 120: | ||
</ | </ | ||
- | The alternative would be to not use an implicit null default value for untyped readonly properties. However, this would be more confusing | + | The alternative would be to not use an implicit null default value for untyped readonly properties. However, this would make the rules for implicit property default values |
- | Specifying an explicitly | + | Specifying an explicit |
<PHP> | <PHP> | ||
class Test { | class Test { | ||
+ | // Fatal error: Readonly property Test::$prop cannot have default value | ||
public readonly int $prop = 42; | public readonly int $prop = 42; | ||
- | |||
- | public function __construct() { | ||
- | // Error, as property is already initialized. | ||
- | $this-> | ||
- | } | ||
} | } | ||
</ | </ | ||
- | This does not appear | + | As the default value counts as an initializing assignment, a readonly property with a default value is essentially the same as a constant, and thus not particularly useful. The notion |
It is worth reiterating here that default values on promoted parameters only apply to the parameter, not the property: | It is worth reiterating here that default values on promoted parameters only apply to the parameter, not the property: | ||
Line 152: | Line 153: | ||
</ | </ | ||
- | As the property has no default value, the assignment in the constructor is initializing, | + | As the property has no default value, the assignment in the constructor is initializing, |
- | Readonly static properties are not supported. This is a technical limitation, in that it is not possible to implement readonly static properties non-intrusively. In conjuction | + | Readonly static properties are not supported. This is a technical limitation, in that it is not possible to implement readonly static properties non-intrusively. In conjunction |
==== Inheritance ==== | ==== Inheritance ==== | ||
- | It is not allows | + | It is not allowed |
<PHP> | <PHP> | ||
Line 169: | Line 170: | ||
} | } | ||
</ | </ | ||
- | |||
- | However, the converse is legal: | ||
<PHP> | <PHP> | ||
Line 177: | Line 176: | ||
} | } | ||
class B extends A { | class B extends A { | ||
- | // Legal: readonly -> readwrite | + | // Illegal: readonly -> readwrite |
public int $prop; | public int $prop; | ||
} | } | ||
</ | </ | ||
- | It is interesting to consider how property redeclaration interacts with the restriction that initializion | + | It is obvious that overriding a readwrite property with a readonly property needs to be forbidden, because that may render operations performed in the parent class invalid. However, this proposal views readonly not just as a lack of capabilities (which would be safe to increase in a child class), but as an intentional restriction. Lifting the restriction in the child class could break invariants in the parent class. As such, a readonly modifier may be neither added nor removed during inheritance. |
+ | |||
+ | It is interesting to consider how property redeclaration interacts with the restriction that initialization | ||
<PHP> | <PHP> | ||
Line 193: | Line 194: | ||
</ | </ | ||
- | Here, initialization of '' | + | Here, initialization of '' |
- | When the same property is imported from two traits, the '' | + | When the same property is imported from two traits, the '' |
<PHP> | <PHP> | ||
Line 210: | Line 211: | ||
</ | </ | ||
- | One could argue that it should be possible to merge with a readonly and a readwrite property into a readwrite property. However, other modifiers currently also require strict equality, for example it is not possible to merge a public and a protected property. If these rules should be relaxed, they should be relaxed consistently. | + | Types on readonly properties remain invariant. One could argue that types of readonly properties |
- | + | ||
- | Types on readonly properties remain invariant. | + | |
<PHP> | <PHP> | ||
Line 223: | Line 222: | ||
</ | </ | ||
- | Covariance would hold for reads from the property, but not for the initializing assignment, which is permitted from both '' | + | Covariance would hold for reads from the property, but not for the initializing assignment, which is permitted from both '' |
==== Unset ==== | ==== Unset ==== | ||
- | TBD | + | Readonly properties cannot be unset once they are initialized: |
+ | |||
+ | < | ||
+ | class Test { | ||
+ | public readonly int $prop; | ||
+ | |||
+ | public function __construct() { | ||
+ | $this-> | ||
+ | unset($this-> | ||
+ | // Error: Cannot unset readonly property Test:: | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | However, it is possible to unset a readonly property prior to initialization, | ||
+ | |||
+ | < | ||
+ | class Test { | ||
+ | public readonly int $prop; | ||
+ | |||
+ | public function __construct() { | ||
+ | unset($this-> | ||
+ | } | ||
+ | |||
+ | public function __get($name) { | ||
+ | if ($name === ' | ||
+ | $this-> | ||
+ | } | ||
+ | return $this-> | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Reflection ==== | ||
+ | |||
+ | A '' | ||
+ | |||
+ | '' | ||
+ | |||
+ | Similarly, closure rebinding can be used to bypass the initialization scope requirement. | ||
+ | |||
+ | ==== Serialization ==== | ||
+ | |||
+ | Readonly properties have no impact on serialization. As '' | ||
+ | |||
+ | This also applies to userland serializers and hydrators. As long as the object is created using '' | ||
===== Rationale ===== | ===== Rationale ===== | ||
- | TBD | + | The readonly property concept introduced in this proposal provides strong immutability guarantees, which apply both inside and outside the class. Once a property has been initialized, |
+ | |||
+ | < | ||
+ | class Test { | ||
+ | public readonly string $prop; | ||
+ | |||
+ | public function method(Closure $fn) { | ||
+ | $prop = $this-> | ||
+ | $fn(); // Any code may run here. | ||
+ | $prop2 = $this-> | ||
+ | assert($prop === $prop2); // Always holds. | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | These guarantees are //too// strong for certain use-cases. For example, some classes may wish to have properties that are publicly readable, but can only be written from within the class. This is a much weaker guarantee, as the value of a property can change during the lifetime of an object. //Both// variants can be useful depending on the situation, and the addition of readonly properties neither precludes nor discourages the addition of asymmetric property visibility. | ||
+ | |||
+ | A special case worth mentioning are classes using clone-based withers: | ||
+ | |||
+ | < | ||
+ | class Point { | ||
+ | public function __construct( | ||
+ | public readonly float $x, | ||
+ | public readonly float $y, | ||
+ | public readonly float $z, | ||
+ | ) {} | ||
+ | |||
+ | public function withX(float $x): static { | ||
+ | // This implementation works: | ||
+ | return new static($x, $this-> | ||
+ | |||
+ | // This implementation does not: | ||
+ | $clone = clone $this; | ||
+ | $clone-> | ||
+ | return $clone; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The clone-based implementation will result in an error, because the '' | ||
+ | |||
+ | In the future, a [[https:// | ||
+ | |||
+ | Alternatively, | ||
+ | |||
+ | This proposal is very similar to the previously declined [[rfc: | ||
+ | |||
+ | This proposal does deviate from the write-once properties RFC in one significant respect: Initialization is only allowed from within the declaring class (modulo the usual rebinding and reflection workarounds). This ensures that these properties are always read-only from a public API perspective, | ||
+ | |||
+ | This also ensures that a potential future "clone with" implementation will only be able to modify readonly properties from private scope and thus cannot bypass additional invariants imposed by the implementation when used from a different scope. | ||
+ | |||
+ | This RFC overlaps with the [[rfc: | ||
+ | |||
+ | It is worth noting that having a readonly property feature does not preclude introduction of accessors. C# supports both readonly properties and accessors. C# also provides properties with implicit backing storage through accessor syntax, but this is not the only way to do it. For example, Swift has special syntax for asymmetric visibility, rather than specifying visibility on implicitly implemented accessors. | ||
+ | |||
+ | Even if we have property accessors, I believe it may be worthwhile to limit them to computed properties only, and solve use-cases that involve engine-managed storage through other mechanisms, such as readonly properties and property-level asymmetric visibility. This avoids confusion relating to the two kinds of accessors (implicit and explicit), and also allows us to make their behavior independent of accessor constraints. For example, a first-class asymmetric visibility feature would shield the user from considering distinctions such as '' | ||
+ | |||
+ | A separate implementation can also be more efficient. After initialization, | ||
===== Backward Incompatible Changes ===== | ===== Backward Incompatible Changes ===== | ||
Line 241: | Line 342: | ||
===== Vote ===== | ===== Vote ===== | ||
- | Yes/No. | + | Voting started on 2021-07-01 and closes on 2021-07-15. |
+ | |||
+ | <doodle title=" | ||
+ | | ||
+ | | ||
+ | </ | ||
rfc/readonly_properties_v2.txt · Last modified: 2021/07/20 15:37 by nikic