Proposal: where T : readonly (struct) #8362
Unanswered
IS4Code
asked this question in
Language Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Motivation
The underlying motivation is pretty much the same as for #3055 (see for background). In short: At the moment, if you have an
in MyStruct x
parameter, callingx.Method()
prevents defensive copying whenMethod
is (at least implicitly)readonly
. The same general concept (being able to call a value type method with the guarantee that the underlying bytes remain unchanged) is impossible to express for anin T x
parameterwhere T : struct, IMyInterface
:Both in CIL and JIT native code (if you disable inlining), the generic
Accept
overload performs a copy even thoughMyStruct
isreadonly
. If inlining is not performed due to various reasons, such code remains unoptimized, which has impact on the performance and possible stack exhaustion if called recursively.In this situation, there are only two options at the moment to solve it:
in
toref
. This requires everyone to pass the argument asref
and thus miscommunicates the intent of the parameter. As a consequence, callers might need to perform their own manual "defensive copying", so this effectively just relegates the issue to the consumers of the method.Unsafe.AsRef
to castin T
toref T
. This could be safe in a controlled environment (for example if you ensure at run-time that the method isreadonly
), but this complicates the code with hidden requirements that the language would otherwise have no troubles enforcing.The general best solution to this problem is a language mechanism that provides the strong guarantee that calling
T.Method
never modifies the bytes that consist the value ofT
. This proposal aims to achieve this through a new generic constraint.Solution
There are two concrete forms of the solution, one controversial and one non-controversial. Starting with the safer option:
where T : readonly struct
By requiring
T
to be areadonly struct
, the language can safely conclude that calling any method onT x
is perfectly safe on areadonly
storage location, and thus no copying is needed.For this constraint,
T
must be declared asreadonly struct
. Optionally, the language might also allow "implicitlyreadonly
" value types (whose all fields arereadonly
), similarly to how it handles theunmanaged
constraint, which could be useful for interaction with older code that does not havereadonly
where it might.where T : readonly
Disclaimer: The section below contains my opinions on how to take this feature further, and the reasoning behind them. Read only if you respect them.
By removing the
struct
constraint,T
is now permitted to be any type that implementsIMyInterface
.Before arriving at the interpretation of what it means for non-
struct
types, let's start with an overview of what we might expect:In the simplest terms, a
readonly
method cannot modifythis
.In more complex terms, the fact that a
struct
's method isreadonly
is a strong language-enforced guarantee that the immediate value of the type remains unchanged. This condition can be easily tested:For all value types in general, the equivalent condition can be expressed as
RuntimeHelpers.Equals(copy, x)
.This condition is something that can be tested universally on any value type, irrespective of its implementation. With
readonly
, the language forces this condition to be always true.The reason
readonly
is a keyword and not a code analyzer-specific attribute is because it gives the consumer of the type greater freedom in using the type, by imposing restrictions on its implementation (for an example of the converse,ref struct
does the opposite ‒ by taking some freedom from the consumer of the type, the implementation can permit more).In terms of storage, there is no difference between
T
andValueTuple<T>
‒ having a variable of either type means it stores a single value ofT
. Thedefault
value, copying, and any comparison done throughRuntimeHelpers.Equals
(with the exception of boxed value types) behaves the same for either type, and the value of one can be easily converted to the other.Under these expectations, I believe
where T : readonly
should be defined as follows:T
, such type is accepted by the constraint only when it is areadonly struct
(possibly implicitly, see above).T
, such type is always accepted by the constraint.Why? Because the only observable effect of a
readonly
member from the consumer's point of view is when it is on astruct
:A hypothetical
readonly
interface member gives no guarantee comparable to that ofstruct
's to a consumer of an instance of such an interface. The only situation where this is beneficial is when the interface is used alongside thestruct
generic constraint ‒ which is precisely what this (or the original proposal) is about!A hypothetical
readonly
class member "could" be defined similarly as forstruct
in that it prevents fields from being modified. However, this is generally impossible to observe at the caller's site:can be transformed into:
Now I could imagine some usefulness in this for code analysis, possibly null-state analysis, such as:
This could be nice, however this is so wildly different from
readonly
that it cannot be considered the same concept.readonly
is all about modifying a storage location, a piece of memory, while this is about semantics, contracts, and code analysis. This is something that could co-exist alongsidereadonly
forstruct
s too!Lastly, the above
Accept
method actually holds for all reference types, because the reference never changes after callingMethod
!This whole argument is based on the actual effect and underlying nature of
readonly
, which is merely extended here in a consistent manner to locations holding other types:readonly
protects are the values of its fields. For a reference type, the value is the reference itself. This is whatRuntimeHelpers.Equals
compares. You would need to go one indirection further to get the class's fields.ValueTuple<T>
holding a reference typeT
, or generally for any value type with a single field which has a reference type, the implications ofreadonly
should be equivalent for both types. The only wayValueTuple<T>
could break thereadonly
invariant is by modifying the reference itself.readonly
member is satisfied by all reference types.readonly
members of value types cannot assign tothis
. No reference type can assign tothis
, thus making it implicitlyreadonly
.The last point also shows that this argument can be made even from the point of view of the producer of such type. Therefore
where T : readonly
should match any reference type, on the basis that no reference type method could possibly modifythis
and no definition ofreadonly
on a class member is conceivable in a manner that gives similar guarantees to the consumers of the class as value types do, thus it makes it a completely different concept, to the point that even if thereadonly
keyword was used for this "informal contract" it would pretty much be a distinct modifier, which, in my opinion, does not justify it being a keyword in this context in the first place.Beta Was this translation helpful? Give feedback.
All reactions