4

This question has kind of been asked before here Why are structs and classes separate concepts in C#? but I'm interested in a specific aspect of this that wasn't really answered.

I'm trying to understand why the language designers for C# and Swift (and possibly others) opted for a language design that promotes implicitly definining a type as a value-type (struct) vs. a reference type (class).

If a language (like C++) only supports defining compound types in one manner, say with the class keyword, but allows instances of this type to be bound to a variable either by value or by reference, this lets the type's consumer decide whether to use value semantics vs. reference semantics.

An oft cited characteristic of good language design is its ability to succinctly convey the most meaning possible in a local manner. Yet, these modern languages seem to conceal the value vs. ref semantics when using them. Exploring this assertion with two examples:

1) Calling a method:

Consider a method that operates on an array in C#. This array is passed as an argument to the method. Since arrays in C# are reference types, the method could either choose to mutate the array directly or to build a new array while operating on copies of the original array's values. Imagine that in both cases, the method would return the reference to that array to the caller in order to support method chaining. Given this scenario, if the method name doesn't hint at mutation or not, the adopted strategy is not readily knowable from the API (i.e. without testing or reading documentation).

Of course in the above example, the method's implementer generally shouldn't mutate the original array without good reason. But the point is that he can and the caller can't know about it just by writing out the method call.

Conversely, if the language design assumes value semantics everywhere (even though it may pass references for performance reasons under the hood, this is an implementation issue), the above example would not be possible. If the method implementor would want to mutate the original array, it would require that the parameter be explicitly marked as a reference to the array, communicating a strong hint to the caller that the array will be mutated.

2) Local variables:

Consider a basic person class with typical members such as name, birth date, etc.

var p1 = new Person();
p1.name = "Tom";
var p2 = p1;     //Note I would never do this but I'm trying to keep the example trivial for brevity.
p2.name = "Peter";

Since Person is a class, only one Person is created and is referenced by both p1 and p2. Therefore, in a more complex example, this indirect mutation of p1 may be unintended. However, if Person was declared as a struct, a copy would be created and p1 would have retained the name of 'Tom'. Again, this requires the type's user to know whether the type was defined as a struct or class - that information is not readily available in the local context.

If the language only supports value semantics by default with explicit references being used, this kind of mistake wouldn't be possible. Things would be even clearer if the language required a different operator for accessing members for refs vs. vals (e.g. -> operator / . operator).

var p1 = new Person();
p1.name = "Tom"; 
var p2 = &p1;
p2->name = "Peter";    //It is pretty clear that we are using a reference here because the language forces a different operator.

So again, given the premise of good language == clear/unambiguous language, why do modern language designers feel that it is better to put reference vs. value type semantics in the hands of the type designer instead of the type consumer?

5 Answers 5

1

If a language (like C++) only supports defining compound types in one manner, say with the class keyword, but allows instances of this type to be bound to a variable either by value or by reference, this lets the type's consumer decide whether to use value semantics vs. reference semantics.

You can do that in C# by merely using classes instead of structures.

Since arrays in C# are reference types, the method could either choose to mutate the array directly or to build a new array while operating on copies of the original array's values. Imagine that in both cases, the method would return the reference to that array to the caller in order to support method chaining. Given this scenario, if the method name doesn't hint at mutation or not, the adopted strategy is not readily knowable from the API

When chaining methods, good practice in C# dictates that the returned array is always a copy, not a reference to the original array. Method chaining is always approached from an immutable perspective. In other words, if I write my method in method chaining style, I should expect a copy of the array to be returned, not a reference to the original array.

If I wanted to mutate the array in place, I would simply pass the array by reference to my mutating function, and return void.

public void TransformArray(T[] array);

If, on the other hand, I were declaring immutable (i.e. copy) semantics, I would return a new array.

public T[] TransformArray(T[] array)

However, if Person was declared as a struct...

In general, the only things that are declared as structs in C# are things that are very likely to have value semantics only, i.e. primitive types. Here is an example:

struct Complex
{
    double Real;
    double Imaginary;
    double Magnitude;
    double Phase;
}

That's about as large as a struct ever gets, unless you're doing something like casting complex data structures in a binary stream, a process that requires bit and word alignment (for which structures are uniquely suited).

4
  • I agree that I could only use classes instead of structs to get this behavior. But since the language permits it, if another programmer on my team uses a struct, I need to know additional information about his type. It's just "one more thing" to check. The issue that I'm getting at is that the language should allow me to know this by looking at my "local context" only. Mar 30, 2016 at 20:02
  • You can hover over the variable in Visual Studio, and it will tell you if it's defined as a struct or a class. Mar 30, 2016 at 20:03
  • I definitely agree with you that method chaining should always be approached from an immutable perspective. However, a beginner may make the mistake of accidentally mutating the reference in his implementation; and the caller can't know this until a bug occurs. Value semantics for parameters would allow the language to mitigate this. Mar 30, 2016 at 20:05
  • I know, I do this all the time (hover over) and it doesn't cause any problems for me. I'm just interested in language design from a theoretical approach. Visual Studio is a tool that provides extra information. I was just wondering if the language, as a stand-alone entity should help more. Mar 30, 2016 at 20:08
1

So again, given the premise of good language == clear/unambiguous language, why do modern language designers feel that it is better to put reference vs. value type semantics in the hands of the type designer instead of the type consumer?

Because putting it in the hands of the type consumer makes the language less clear - and because that's not the only premise involved in language design.

First, clarity. The problem with allowing users to specify the pass by ref/value is that now you have inconsistency. You need to remember how that Foo was declared this time. The argument is that such inconsistency harms the clarity of the language, and adds cognitive burden on the developer.

Why else? The structure of C# types differ depending on if they're reference or value types to aid in their marshaling (for value types) or dispatch (for reference). For C# in particular, auto-generated equality and hash code operators will differ depending on what mode you're in. If you allow either passing mode, you can't optimize the compiler for this stuff.

And in the end - it doesn't much matter. In modern C#, value types are largely frowned upon in favor of immutable reference types. Even at their peak, maybe 1 in 50 types were actually value types. Why complicate the language further for something nobody's really going to use?

3
  • Interesting answer. Thanks. My whole point is about reducing cognitive burden so you're on to something. Maybe I'm the one who is weird, but although I agree with you that there is inconsistency with having the option for call by ref vs. val, it is "in your face" so I find that it is of a lower burden (less to remember than knowing which types are what). Note that in order for this to be "in your face", I'm suggesting a different operator for accessing members -> instead of dot operator. The code editor / compiler would help distinguish the two. Mar 30, 2016 at 20:25
  • I agree that structs are rare in C#. What really got me thinking about this is that I've been learning Swift for iOS development and they've adopted the same approach. However, contrary to C#, Apple is pushing for value types as much as possible. Mar 30, 2016 at 20:26
  • As an example to my above comment, in Swift, arrays, strings and dictionaries are value types. Mar 30, 2016 at 21:51
1

The reference/value semantics is put in the hands of the type designer because this makes the language much simpler for the consumer of the types. In practice the consumer does not need to understand the subtle difference between value and reference types but can treat them equally, which makes the language much simpler conceptually (and syntactically).

In practice, C# only supports reference semantics. True, we know some types like integers are not (always) created on the heap, but this is an implementation detail - there would be no observable difference (except performance) on the language level if integers were always created on the heap just like other objects.

The difference is not observable because all the built-in value types (integers, Point etc.) are immutable. If value types were mutable you would be able to observe the difference since changes are not shared the same way as for class instances.

Now there is a loophole in this logic since you are able to defines your own structs which are mutable. However, defining structs is considered an advanced feature, and it is considered bad practice to define mutable structs. (Your example (2) is only possible if Person is a mutable struct.)

So unless you deliberately shoot yourself in the foot by creating a mutable value type, C# has a single, consistent copying and passing semantics: Objects are shared.

So why have value types in the language at all? This is a performance tradeoff. It would be to much overhead to create integer values on the heap (and having to collect them again). So small, frequently used (immutable) types are defined as value types which allows some optimizations. Sometimes it is said that value types are placed on the stack (as opposed to the heap), but I believe this is a simplification and they might be optimized to exist purely in a processor register.

Bottom line: The type consumer does not have the choice between treating a type as reference or value type because the language becomes a lot simpler conceptually and syntactically if there is a single consistent model.

1

In the second example, what are the types of p1 and p2? Are they the same or are they different? Is it allowed to use one in place of another? My understanding (e.g. from the remark that different syntax is used to access members of the class) is that the types are different and cannot replace each other.

If this is indeed the case, the most essential reason to keep the difference between reference and value semantics in hands of type designer is reuse. With this approach, there is no need to have different code to handle reference or value types, they can be used in a uniform way. For example, a generic method (in an imaginary syntax)

void sort (data: Array [T -> Comparable])

allows to sort an array of comparable elements that may be of either reference or value type. With the distinction at the type consumer side it would require two methods:

void sort (data: Array [T -> Comparable])
void sort (data: Array [&T -> Comparable])

or something like that.

Moreover, in the latter case it's hard to avoid using formal generic type parameters and have just

void sort (data: Array [Comparable])

that may be OK in the former case depending on the typing rules.

0

It's a complete overreaction to slicing. Slicing doesn't happen to anybody but beginners, unless you do so intentionally, but people feel the need to ban it in their languages anyway. This is typically achieved by forcing value types to be separated and implementing separate rules for them- for example in C# you cannot have structs that inherit from other structs.

The whole point of the separation is to restrict value types.

All of this is so that on their first day, people can't accidentally slice.

6
  • I don't understand what "slicing" has to do with the OP's question. Mar 30, 2016 at 19:29
  • Because slicing is what you get if you let value types have interesting semantics. The whole point of separating out value types and reference types is to ban value types from having interesting semantics to prevent slicing- see the answer on the other question.
    – DeadMG
    Mar 30, 2016 at 19:30
  • 1
    Ah, that kind of slicing. Seems like a peripheral concern to me. Mar 30, 2016 at 19:33
  • I agree that it's basically completely irrelevant in every way and pretty much solely a theoretical problem; it's amazing how badly people fuck up their languages to avoid it.
    – DeadMG
    Mar 30, 2016 at 19:34
  • en.wikipedia.org/wiki/Object_slicing. Not the only thing that Microsoft excluded from C# (you don't get multiple class inheritance either). Mar 30, 2016 at 19:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.