Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C# Design Notes for Jul 1, 2015 #3913

Closed
MadsTorgersen opened this issue Jul 12, 2015 · 44 comments
Closed

C# Design Notes for Jul 1, 2015 #3913

MadsTorgersen opened this issue Jul 12, 2015 · 44 comments

Comments

@MadsTorgersen
Copy link
Contributor

C# Design Meeting Notes for Jul 1, 2015

Agenda

We are gearing up to prototype the tuple feature, and put some stakes in the ground for its initial design. This doesn't mean that the final design will be this way, and some choices are arbitrary. We merely want to get to where a prototype can be shared with a broader group of C# users, so that we can gather feedback and learn from it.

Tuples

The tuples proposal #347 is pretty close to what we want, but has some open questions and unaddressed aspects.

Names

We want the elements of tuples to be able to have names. It is exceedingly useful to be able to describe which elements have which meaning, and to be able to dot into a tuple with those names.

However, it is probably not useful to make the names too strongly a part of the type of a tuple. There is no really good reason to consider (int x, int y) an fundamentally different tuple type than (int a, int b). In fact, according to the analogy with parameter lists, the names should be of secondary importance: useful for getting at the elements, yes, but just as parameter lists with different parameter names can overwrite each other, as long as the types match at the given parameter positions, so should tuple types be considered equivalent when they have the same types at the same positions.

Another way to view it is that all tuples with the same types at the same positions share a common underlying type. We will make that type denotable in the language, in that you can write anonymous tuple types, (int, int), even though you cannot write an anonymous parameter list.

For now we don't think we will allow partially named tuples: it is either all names or none.

(int x, int y) t1 = ...;
(int a, int b) t2 = t1; // identity conversion
(int, int) t3 = t1;     // identity conversion

All "namings" of a tuple type are considered equivalent, and are convertible to each other via an identity conversion. For type inference purposes, an inferred tuple type will have the names if all "candidate types" with names agree on them, otherwise it will be unnamed.

var a1 = new[]{ t1, t1 }; // infers (int x, int y)[], since all agree
var a2 = new[]{ t1, t2 }; // infers (int, int)[], since not all agree
var a3 = new[]{ t1, t3 }; // infers (int x, int y)[] since all with names agree

For method overriding purposes, a tuple type in a parameter or return position can override a differently named tuple type. The rule for which names apply at the call site are the same as those used for named arguments: the names from the most specific statically known overload.

Tuple literals likewise come in named an unnamed versions. They can be target typed, but sometimes have a type of their own:

var t1 = ("Hello", "World");           // infers (string, string)
var t2 = (first: "John", last: "Doe"); // infers (string first, string last)
var t3 = ("Hello", null);              // fails to infer because null doesn't have a type
var t4 = (first: "John", last: null);  // fails to infer because null doesn't have a type

(string, string) t5 = ("Hello", null);                           // target typed to (string, string)
(string first, string last) t6 = ("John", null);                 // target typed to (string first, string last)
(string first, string last) t7 = (first: "John", second: "Doe"); // error: when given, names must match up
(string first, string last) t8 = (last: "Doe", first: "John");   // fine, values assigned according to name

The last two are probably the only possibly controversial examples. When target typing with names in the literal, this seems very similar to using named arguments for a method call. These rules match that most closely.

This is something we may want to return to, as it has some strange consequences. For instance, if we introduce a temporary variable for the tuple, and do not use target typing:

var tmp = (last: "Doe", first: "John"); // infers (string last, string first)
(string first, string last) t8 = tmp;   // assigns by position, so first = "Doe"

But for now, let's try these rules out and see what they feel like.

Encoding

Are core question is what IL we generate for tuples. The equivalence-across-names semantics make it easy to rely on a fixed set of underlying generic library types.

We want tuples to be value types, since we expect them to be created often and copied less often, so it's probably worthwhile avoiding the GC overhead.

Strangely perhaps, we want tuples to be mutable. We think that there are valid scenarios where you want to keep some data in a tuple, but be able to modify parts of it independently without overwriting the whole tuple. Also, calling methods on readonly tuple members that are themselves value types, would cause those methods to be called on a throw-away copy. This is way too expensive - it means that there may be no non-copying way of doing e.g. value-based equality on such tuples.

So we encode each tuple arity as a generic struct with mutable fields. It would be very similar to the current Tuple<...> types in the BCL, and we would actively avoid purely accidental differences. For this reason, the fields will be called Item1 etc. Also, tuples bigger than a certain arity (probably 8) will be encoded using nesting through a field called Rest.

So we are looking at types like this:

public struct ValueTuple<T1, T2, T3>
{
    public T1 Item1;
    public T2 Item2;
    public T3 Item3;
}

Possibly with a constructor, some Equals and GetHashCode overrides and maybe an implementation of IEquatable<...> and IStructurallyEquatable<...>, but at its core exceedingly simple.

In metadata, a named tuple is represented with its corresponding ValueTuple<...> type, plus an attribute describing the names for each position. The attribute needs to be designed to also be able to represent names in nested tuple types.

The encoding means that an earlier version of C#, as well as other languages, will see the tuple members as Item1 etc. In order to avoid breaking changes, we should probably keep recognizing those names as an alternative way of getting at any tuple's elements. To avoid confusion we should disallow Item1 etc. as names in named tuples - except, perhaps, if they occur in their right position.

Deconstruction syntax

Most tuple features have a deconstruction syntax along the following lines:

(int sum, int count) = Tally(...);   

If we want such a syntax there are a number of questions to ask, such as can you deconstruct into existing fields or only newly declared ones?

For now we sidestep the question by not adding a deconstruction syntax. The names make access much easier. If it turns out to be painful, we can reconsider.

Other issues

We will not consider tuples of arity zero or one - at least for now. They may be useful, especially from the perspective of generated source code, but they also come with a lot of questions.

It seems reasonable to consider other conversions, for instance implicit covariant conversions between tuples, but this too we will let lie for now.

@Bill-McC
Copy link

Would the vb syntax be :
dim t1 = ("Hello", "World")
I guess it can't be { }'s, so would have to be ( )'s
So assuming that gives us this:
Dim t8 As (first As String, last As String) = (second:= "Doe", first:= "John")
And that is an error, but the following would compile and work, but would give first as Doe, last as John
Dim t8 As (first As String, last As String) = ((second:= "Doe", first:= "John"))

Maybe it might be nice for named tuples to require an explicit cast ?

Also will vb be case insensitive on this, and c# case sensitive ? Would casing differences result in an error in c#, but work fine in vb ?

@Bill-McC
Copy link

Let's say we have tuple (int, int, int, int) such as left, top, right, bottom. Is it really desirable to allow implicit cast to left, top, width, height ?

@dsaf
Copy link

dsaf commented Jul 13, 2015

@MadsTorgersen

For type inference purposes, an inferred tuple type will have the names if all "candidate types" with names agree on them, otherwise it will be unnamed.

  1. Wouldn't it make sense to also look at Stronger inference of a common type of a set of expressions #1419 and [C# feature] Infer generic type arguments in collection initializers #1470 to make the collection type inference experience generally more coherent?
  2. Will conversion to the current Tuple<...> type be transparent and implicit? Are inferred property names going to be Item1, Item2 etc. when using them in a collection initializer together with new tuples?
  3. Would (string name, object creature) be compatible with (string name, Creature creature)? What about (string name, ICreature creature)?
  4. Is it planned to make tuples optionally immutable via other new language features?

We want tuples to be value types, since we expect them to be created often and copied less often, so it's probably worthwhile avoiding the GC overhead. ... Strangely perhaps, we want tuples to be mutable.

  1. Isn't this more valid when said about anonymous types though? Do you plan to make them compile to struct as well now? Mutable anonymous types that can be passed around - I cannot see how this would not be abused.
  2. Are new generic constraints planned for tuples e.g. where T : tuple or where T : tuple(int, int)?
  3. Will I be able to tell in a direct way whether a type is a tuple when using reflection?
  4. Would you be willing to add any helper methods such as GetItem(int index) and int ItemCount { get; } for flexible yet fast tuple scenarios?

@paulomorgado
Copy link

I still don't see any value on the tuples themselves having names. I think that adds unnecessary complexity.

If the implementation is kept simple and tuples are tuples (something that has Item1..ItemN properties) the implementation will be pimplier.

If deconstrucion is the rule of law to name the items of the tuple, there's no need for the tuple items having names. Regardless of what he compiler can do with names.

The only place where item names is useful is when describing the usage of the tuples and tooling (refactoring).

From the top of my mind, these places are:

  1. return values,
  2. parameters
  3. fields and properties

The naming could be solved by applying an attribute with the list of item names to element with a tuple value. That list of names can be an array, which has no problem with the number of elements it can have and perfectly compatible with the nesting of tuple definitions.

This would also lift the restrition on names like Item1.

Is there any compelling example that can't be served by this theory?

@HaloFour
Copy link

@paulomorgado

Names are self-documenting. As described above the names of the tuple properties is akin to that of the names of parameters; they are not a part of the contract but they are still important in describing the contract. I'd personally prefer that the names be more formal as actual properties, but without some mechanism for structural equivalence in the CLR this is the best option.

As for the array/indexer concept, you could do that today using object[], and it would be awful since there is no type-safety and all value types must be boxed. In order to have proper type-safe generic properties the tuple type must expose each value as a separate property. The CLR has no concept of exposing an indexer of heterogeneous data types. The BCL/C# compiler could always just use numbers as names and hide that detail behind some syntax like foo[0] or foo.$0 but that would require that the BCL type violate CLS.

@paulomorgado
Copy link

@HaloFour, I never criticised the use of names nor advocated for an array/indexer API.

I even gave a suggestion on how names could be attached to the usage of the tuple without being attached to the tuple itself.

You can write somnething like this:

public (bool hasValue, int value) TryParse(string text)
{
    int value;
    bool hasValue = int.TryParse(text, out value);
    return (hasValue, value);
}

And it will be translated to:

[return: TupleNames("HasValue", "Value")]
public ValueTuple<bool, int> TryParse(string text)
{
    int value;
    bool hasValue = int.TryParse(text, out value);
    return ValueTuple.Create(hasValue, value);
}

Any tooling can pick up the names. And it's cross-assembly.

On the caller side, you can either write:

var res = TryParse("123");
if (res.Item1)
{
    Console.WriteLine(res.Item2);
}

or:

(var hasValue, var value) = TryParse("123");
if (hasValue)
{
    Console.WriteLine(value);
}

With an extra effort, this would be possible:

var res = TryParse("123");
if (res.$hasValue)
{
    Console.WriteLine(res.$value);
}

@svick
Copy link
Contributor

svick commented Jul 14, 2015

@paulomorgado From what you wrote before, it wasn't clear to me that that was what you meant, it sounded like you wanted users to write that attribute themselves.

What's also confusing is that as far as I can see, what you're saying is exactly the same thing as what @MadsTorgersen proposed, but you seemed to be saying you disagree with the original proposal.

@HaloFour
Copy link

@paulomorgado Oh sorry, you said that the list of names could be an array. Yeah, it could be.

Adding "pseudo-properties" to the inferred tuple would be a nice convenience, too. That would be a novel use case for the proposed .$ indexing operator.

@paulomorgado
Copy link

One of the things I disagree with is this:

In metadata, a named tuple is represented with its corresponding ValueTuple<...> type, plus an attribute describing the names for each position. The attribute needs to be designed to also be able to represent names in nested tuple types.

Attaching the names to the exposing element (return value, parameter, field, etc.) removes the need to attach it to the value itself. After all, the names are meaningless for tuple equivalence.

@HaloFour
Copy link

@paulomorgado

Attaching the names to the exposing element (return value, parameter, field, etc.) removes the need to attach it to the value itself.

I believe that is what the proposal is saying, the attribute would be on the return, parameter, etc.

public (name: (first: string, last: string), address: (street: string[], city: string, county: string, country: string) GetPerson((id: int, string: search) param) {
    ...
}
[return: Tuple("name", "first", "last", "address", "street", "city", "county", "country")]
public ValueType<ValueType<string, string>, ValueType<string[], string, string, string>> GetPerson([Tuple("id", "search")] ValueType<int, string> param) {
    ...
}

@JesperTreetop
Copy link

Let's say we have tuple (int, int, int, int) such as left, top, right, bottom. Is it really desirable to allow implicit cast to left, top, width, height ?

I agree with @Bill-McC's concern.

Tuples are a good and necessary break from nominal typing. But if you can give tuple elements names, those names are integral to the structure of the tuple - they dictate what type this is and conveys semantics and intent just as much as, or even more than, the types of the elements.

In this way, they would also make sense in comparison to anonymous types where not only the type matters but also the name as well as the position. A little flexibility is gained by allowing name decay, but at the expense of making them less robust building blocks.

I am also definitely in favor of the nameless tuples (with no element names), where you explicitly strip any semantics and intent other than that conveyed by the types. Being able to say that is valuable too.

Allowing you to send an (width: int, height: int) tuple into a function taking (x: int, y: int) does not make sense. It seems that some measures will be taken to prevent this, but that if going by a temporary variable, this could be circumvented. This is a bug farm. I want tuples to be lightweight and low on ceremony, not flimsy. Please require the names to match up at all times if given and keep this information around; that way, it will never be the source of any gotchas even if it means a bit more typing, and reordering could be included in a sane way.

@HaloFour
Copy link

@JesperTreetop If anything I'd say those methods are bug farms with or without named elements. Tuples have their uses, but I think that this example is exactly where nominal typing is significantly more appropriate. What scares me that is that tuples will be slopped around in place of nominal types by people who think that saving a couple of keystrokes anywhere and everywhere is somehow a good idea.

@paulomorgado
Copy link

+1, @HaloFour

@dsaf
Copy link

dsaf commented Jul 20, 2015

@JesperTreetop

Allowing you to send an (width: int, height: int) tuple into a function taking (x: int, y: int) does not make sense.

Actually I can see some uses in graphics or game programming, where size can easily be used as a vector. I would give a different example: (age: int, numberOfPeople: int) vs (numberOfPeople: int, age: int) vs (numberOfPeople: int, dayOfMonth: int).

@HaloFour

What scares me that is that tuples will be slopped around in place of nominal types by people who think that saving a couple of keystrokes anywhere and everywhere is somehow a good idea.

I can easily see that happening. Besides, there seems to be a strong overlap with the new record type and decomposition - covering both succinctness and flexibility.

@JesperTreetop
Copy link

What scares me that is that tuples will be slopped around in place of nominal types by people who think that saving a couple of keystrokes anywhere and everywhere is somehow a good idea.

@HaloFour: it is already happening. This is what people use anonymous types for and they fall off a cliff beyond which they will use hacks. A sane tuple design would prevent bugs and not cause them. We already have a weak tuple design with System.Tuple. It's time for the one people are asking for too.

@dsaf
Copy link

dsaf commented Jul 20, 2015

@JesperTreetop

Why would one use named tuples over records?

@JesperTreetop
Copy link

@dsaf: Fair question.

If records are going to be in, they will likely be a better alternative than tuples with named fields. (I was under the impression that records were coupled with pattern matching as a long shot that probably won't make C# 7.) But tuples with named fields whose names can go away or that don't hold, in the way as demonstrated in the actual notes, are brittle and invite mistakes. At least tuples that don't have field names honestly reflect that nothing further is known, much like indexing into an array at a position by a bare number shows that there's no other structural information available.

I would love records, and I would love having both records and tuples, but regardless of whether records make it or not, I would not love tuples that look like they could be used as records but that fall down in subtle ways when you do. I don't see the appeal with that at all. Don't provide "half" a feature, just drop the names at that point. If records do end up making it, and I hope they do, you're right, record syntax + tuples with unnamed elements only is probably the best combination.

@paulomorgado
Copy link

I would like to point out that giving names to tuple fields is not the same as the fields having names.

Type (class or struct) fields have names even after compiled to IL. Method parameters and local variables don't. You name them on the source code but they don't have names on the IL.

I don't think there's any value on tuple fields having names given that they are intended to have structural equivalence.

I do see a lot of value in accessing them by name on source code.

@dmikov
Copy link

dmikov commented Jul 21, 2015

I have to say, I really don't understand the decision. Making tuples act like objects during consumption is a "why not objects" for me. Even in proposal it was indicated, that treating tuples/return values like real objects is strange, they are not logical units.
Is it better then current Item1 - yes, but not much saved compare to struct declaration. Having decomposition on even current tuples would've been much more natural, giving different choices of syntax.
I would've been happy if "return (c1,c2)" will create a tuple that I can cast/decompose to (resultCode, count) = ...
P.S I don't understand the decomposition confusion - new or existing. What are you planning for the returned named tuple that you propose? New or existing, do the exactly same with variables.

Also named are set at the method, what if I write generic method used by many domains? It might be averageAge in one or averagePrice in the other module. Decomposition will have it, the named will have to be cast to variable or remember that result.Average might mean average price, since I sent Price type.

@KalitaAlexey
Copy link

How about to forbid tuples and allow to return anonymous object?
To deconstruct the following syntax can be used:

{ Name: string, Names: List<string> } ReceiveData()

// variable names match
Name, Names = ReceiveData()
AnotherName <- Name, AnotherNames <- Names = ReceiveData()

@paulomorgado
Copy link

@KalitaAlexey, how is that any better?

A tuple is a type that already exists and the type of tuple with two ints is the same as the type of any other tuple with two ints.

@KalitaAlexey
Copy link

Anonymous object is better, because it more clear to get values from anonymous object in comparison with tuple
.Name makes more sense, than
.Item1

@paulomorgado
Copy link

@KalitaAlexey, you should read the whole thread.

@KalitaAlexey
Copy link

@paulomorgado I read the whole thread, but I think

Name, Names = ReceiveData()
AnotherName <- Name, AnotherNames <- Names = ReceiveData()

is clearer than

(string name, List<string> names) = ReceiveData()

because when values of a tuple has different types it works good, but for something like

(int id, int age) GetUserData()

someone could write

(int age, int id) = GetUserData()

and it is going to compile, but is not going to work properly.

@whoisj
Copy link

whoisj commented Jul 22, 2015

@KalitaAlexey you're suggestion doesn't solve the problem you've expressed. I'm confuse. Enlighten.

@KalitaAlexey
Copy link

@whoisj You are wrong.
With tuple you can write

(int id, int age) = GetUserData()

or

(int age, int id) = GetUserData()

but with anonymous object for function

{ int Id, int Age } GetUserData()

it is available clear variants

int id, int age = GetUserData()

or

int age, int id = GetUserData()

but here all fields will be match, because their names match rules. And also is available something like

int anotherUserAge <- Age, int anotherUserId <- Id = GetUserData()

It is more flexible, doesn't it?

@dsaf
Copy link

dsaf commented Jul 23, 2015

@KalitaAlexey What prevents tuples from having the kind of unimplemented decomposition capability that you are describing?

@KalitaAlexey
Copy link

@dsaf Tuple by default it is a values of possible different types. Only order is matter. I suggest to think that name is matter, but not order

@JohnnyBravo75
Copy link

@KalitaAlexey

But the samples from Mads in the NAMES section say:

"
(string first, string last) t7 = (first: "John", second: "Doe"); // error: when given, names must match up
(string first, string last) t8 = (last: "Doe", first: "John"); // fine, values assigned according to name

The last two are probably the only possibly controversial examples. When target typing with names in the literal, this seems very similar to using named arguments for a method call. These rules match that most closely."

So when names are given, they have to match, not the position.
So this ist what you wanted, the same as your suggestion with the anonymous types.

@qrli
Copy link

qrli commented Aug 11, 2015

By rethinking about it, I find it easier to think it as parameter list.

  • Syntax-wise, it is the same as a parameter list. Its value syntax is also same as argument list.
  • So, without names, it is positional. e.g. (true, 15)
  • With names, the order can be flexibile. e.g. (hasValue: true, age: 15)
    These matches the design of OP very well.

Then what does not exist before is the tuple return value. It of course can be the returned ValueTuple object, but that does not work the same as parameter list. Because it combines elements into a structure. This also creates casing delimma, as public structure member should be PascalCasing per standard but tuple member and parameter list uses camelCasing.

Then the idea of treating the return value and out parameters as the return tuple catch me. It is originaly raise by HaloFour? I don't remember clearly. But it matches the analogy very well. So the return tuple is still parameter list, the out portion of parameter list.
So
(bool suc, int value) = dict.TryGetValue(key)
would be equavalent to
int value; bool suc = dict.TryGetValue(key, out value)
to receive result as tuple, it would be
var tuple = ValueTuple.From(dict.TryGetValue(key))
var tuple = dict.TryGetValue(key)

Just some thoughts.

@paulomorgado
Copy link

@qrli,

So

(bool suc, int value) = dict.TryGetValue(key)

would be equavalent to

int value; bool suc = dict.TryGetValue(key, out value)

That can't be! Regardless of the questionable syntax, tuples have positional semantics.

Imagine this API:

(int x, int y, int z) GetPosition();

Regardless of the name you give to the tuple items (or even if you give them a name at all), the first item will be x, the second will be y and the third will be z.

@qrli
Copy link

qrli commented Aug 12, 2015

@paulomorgado
I think you misread my example. My example is indeed positional.
The line
int value; bool suc = dict.TryGetValue(key, out value)
is not tuple syntax but current normal C# syntax. It is clearer if I wrote in two lines:

int value; 
bool suc = dict.TryGetValue(key, out value);

@paulomorgado
Copy link

You're right! Sorry!

@asik
Copy link

asik commented Aug 25, 2015

If tuples are mutable structs they will come with the usual pitfalls of mutable structs, i.e. http://blogs.msdn.com/b/ericlippert/archive/2008/05/14/mutating-readonly-structs.aspx .

In addition, tuples should be structurally equatable, but that leads to strange behavior if they are also mutable. In general, structurally equatable types (types that represent values) should be immutable, and good principles shouldn't be violated without good reason.

The performance penalty of copying struct tuples seems overblown, the same mistake that gave us reference type tuples originally (http://stackoverflow.com/a/5856728/154766). Tuples are useful as small records and should typically not contain more than a few elements; they should be designed and optimized around that common case. Anything larger would benefit from being promoted to a real class or record (if that makes it in C#7).

For instance, it should not be a problem to have to "overwrite the whole tuple", because tuples are small data structures.

Also, calling methods on readonly tuple members that are themselves value types, would cause those methods to be called on a throw-away copy. This is way too expensive - it means that there may be no non-copying way of doing e.g. value-based equality on such tuples.

Perhaps compiler-generated equality code can pass the structs by reference. User-defined equality would be problematic, but that seems a strange feature to support for tuples.

@whoisj
Copy link

whoisj commented Aug 25, 2015

If tuples are mutable structs they will come with the usual pitfalls of mutable structs, i.e.

"useful pitfalls", there I corrected that for you.

types that represent values should be immutable

So x += 2; should be completely illegal because values are immutable, right?

@asik
Copy link

asik commented Aug 25, 2015

So x += 2; should be completely illegal because values are immutable, right?

You confuse values and variables. 2 += 1 should be (and is) illegal because 2 is a value; x += 2 is legal if x is a mutable variable.

@HaloFour
Copy link

Actually, int is immutable. You're replacing the variable with a
completely new value, not modifying it in place. A mutable struct is one
that can be changed by updating a property or field on it either directly
or though a method. For example:

Point p = new Point(2, 2);
p.Offset(2, 2);
p.X = 5;

Debug.Assert(p.X == 5);
Debug.Assert(p.Y == 4);

@whoisj
Copy link

whoisj commented Aug 25, 2015

Actually, int is immutable. You're replacing the variable with a
completely new value, not modifying it in place.

Really, the compiler is allocating a new bit of memory every time I ask it to write to a value? I'm skeptical.

p.X=5

This is just writing bits to memory. How different is it from int x = -1; x &= 0xFFFF0005;?

Neither is pretty, but both are useful and necessary (in the correct context).

@HaloFour
Copy link

@whoisj The difference is that you're overwriting the entire memory space with a new value, not a portion of it. int doesn't offer any members to modify the underlying value directly, you have to modify and replace, so it is effectively immutable.

@whoisj
Copy link

whoisj commented Aug 25, 2015

@HaloFour I see where you're coming from now.

Are you suggesting that struct should only be written to if the entire struct is written to?

Example:

Point p = new Point(2, 2);
p = p.Offset(2, 2);
p = new Point(5, p.Y);

so it is effectively immutable

So long as nobody has a ref to it. 😉

@HaloFour
Copy link

@whoisj

Are you suggesting that struct should only be written to if the entire struct is written to?

Yes, because when you pass a struct you are passing the entire value. Requiring that the struct be completely rewritten avoids those cases where you think that you can mutate a struct but you're actually mutating a throw-away temporary value. For example:

public class Foo {
    public Point Point { get; set; }
}

static class Program {
    static void Main() {
        var foo = new Foo() {
            Point = new Point(2, 2)
        };
        foo.Point.X = 5;
        Debug.Assert(foo.Point.X == 2);  // didja see that one coming?
    }
}

So long as nobody has a ref to it. 😉

This is perfectly fine because you'd at least be updating the reference to the new complete value.

@whoisj
Copy link

whoisj commented Aug 25, 2015

@HaloFour I'd support that if it were enforced. So long as they remain writable and I could still partially write with unsafe. Actually, why aren't structs done like that and why aren't more types structs?

@asik
Copy link

asik commented Aug 26, 2015

It is easy to see how confusing mutable struct tuples would be in a language that defaults to reference types. This would be weird to most C# programmers:

void Foo(Tuple<int, string> tuple) {
    // some code
    tuple.Item2 = ""; // caller doesn't see the mutation, it's only local
}

With immutable tuples, if the programmer really wants a different local copy, he has to make this explicit, and now there's no confusion possible:

void Foo(Tuple<int, string> tuple) {
     var localTuple = (tuple.Item1, "");
}

Or let's say arr is an array of Tuple<int, string>:

    for (int i = 0; i < arr.Length; ++i)
    {
         var elem = arr[i];
         elem.Item1 = 0;
         elem.Item2 = "";
    }

This is reasonable-looking code that compiles without warning, but it doesn't actually mutate the elements of the items in the list; it only mutates the local variable elem. If tuple elements are readonly, this would fail to compile, and one has to write the correct:

    for (int i = 0; i < arr.Length; ++i)
    {
        arr[i] = (0, ""); // or whatever the syntax is for tuple creation
    }

@gafter
Copy link
Member

gafter commented Apr 25, 2016

Design notes have been archived at https://github.com/dotnet/roslyn/blob/future/docs/designNotes/2015-07-01%20C%23%20Design%20Meeting.md but discussion can continue here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests