In C♯, pass-by-value is the default, but pass-by-reference is also available, and has to be explicitly annotated at both the declaration site and the call-site:
void transmogrify(Foo foo, ref Bar bar) { /* do something */ }
Foo foo;
Bar bar = new Bar();
transmogrify(foo, ref bar);
C♯ even distinguishes between two different kinds of pass-by-reference: ref
and out
. out
parameters, as the name implies, are intended specifically for returning values: the caller is not required to initialize them first, and the callee must initialize it in all possible paths through the method (and it is a compile error if it doesn't). The callee is also not allowed to dereference it. ref
s OTOH must be initialized by the caller before calling the method.
Note that this only applies to the variable binding: normal pass-by-value parameters cannot modify variable bindings in the caller's scope, ref
and out
parameters can. However, C♯ also distinguishes between value types and reference types. Value types are copied when passed, but for reference types only the pointer is copied. So, you can have an "input" parameter of a reference type, and while you cannot change the variable binding in the caller's scope, you can mutate the object the variable binding points to.
This is still pass-by-value (sometimes called call-by-object-sharing, call-by-sharing, or call-by-object).
So, you have:
- no annotation: pass-by-value
ref
: pass-by-reference, both directions
out
: pass-by-reference, only output
and
- value types: a copy of the value is passed directly
- reference types: a copy of the pointer to the shared value is passed
And all 6 combinations are legal.
The closest thing you are looking for, is to restrict yourself to using only value types, and only pass-by-value or out
. That way, you can be sure that your inputs will never be modified (because they are copied), and that your by-reference parameters will only be returned (because you don't even have to initialize them, there is nothing to change anyway).
Note that in modern languages, there are often alternatives to returning multiple values via pass-by-reference.
One often used pattern for returning multiple values is that one value denotes success or failure and the other value is the actual return value, which is only set when the method succeeds. In a language which supports parametric polymorphism aka generics or templates, this can be much better expressed with an Option<T>
type. An Option<T>
is a type that represents a value of type T
that may or may not be present. It is kind-of like a collection that can only have 0 or 1 element. It is also a monad. If you have Option<T>
implement your language's standard collection and monad APIs, it can be very nicely and naturally used. Typical usage looks like this:
var maybeInt = int.TryParse("23");
var myInt = maybeInt.Get // unsafe, may throw an exception!
if (maybeInt.isPresent) maybeInt.Get // safe
var myInt = maybeInt.GetOrElse(0) // safe, provides default for missing value
foreach (var i in maybeInt) // using the collection interface
{
// do something with i
// will only be executed if value is present
}
// using the monad interface
from i in maybeInt select /* do something with i */
Another common pattern for returning multiple values is that one value denotes an error code and the other value is the actual return value, which is only set when the method succeeds. This allows to pass more information than a simple Option<T>
which can only encode presence or absence. In a language which supports parametric polymorphism aka generics or templates, this can be much better expressed with an Error<T>
type (also sometimes called Try<T>
). An Error<T>
is a type that represents either a value of type T
that may or may not be present or an error code value. It is kind-of like a pair of collections that each can only have 0 or 1 element and exactly one of the two collections has 1 element, the other has 0. You can also a make it a monad, by biasing it to consider only the "value" collection. If you have Error<T>
implement your language's standard collection and monad APIs, it can be very nicely and naturally used. Normally, if you don't care about the exact error, you can use it exactly like Option<T>
. It will execute foreach
or Select
if the computation was a success and ignore it if it wasn't. There is an unsafe Get
and a safe GetOrElse
, there is isSuccess
for checking.
var intOrError = int.TryParse("23");
var myInt = intOrError.Get // unsafe, may throw an exception!
if (intOrError.isSuccess) intOrError.Get // safe
var myInt = intOrError.GetOrElse(0) // safe, provides default in case of failure
foreach (var i in intOrError) // using the collection interface
{
// do something with i
// will only be executed if computation succeeded
}
// using the monad interface
from i in intOrError select /* do something with i */
if (intOrError.isFailure) intOrError.GetError // inspect the error
If the language has exceptions, it is also possible to model an error code return value with an exception instead. It depends on whether failure is a normal occurrence (in that case use Option
or Error
) or is a truly exceptional situation that should not happen, then throw an exception.
For cases where none of the above applies, and you really truly need to return multiple unrelated values, you can always wrap those values into a tuple, record, struct, collection, or object. Many languages have lightweight syntax for that. In future versions of C♯, we will very likely have lightweight syntax for tuples and records, and a limited form of pattern matching for variable assignments, so the need to use pass-by-reference to return more than one value will be greatly reduced. E.g., the current TryParse
method which currently looks like this:
int i;
bool success;
success = int.TryParse("42", i);
if (success) { /* do something with i */ }
could be replaced by
var (success, i) = int.TryParse("42");
and within the implementation of TryParse
, there would be a statement like
return (success, i);
ref
orout
when you pass by reference. (But you can still pass references by-value without annotations, the benefit is rather limited)