-
Notifications
You must be signed in to change notification settings - Fork 5.9k
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
Define "source breaking change", "binary breaking change" in the context of language versioning. #3717
Comments
No. It is both safe from a source and binary perspective. CoreFx is already making progress in converting a number of structs to be
This is safe from a source perspective but breaking from a binary perspective. The
For locations where binary compat is important you can simply add an overload that takes |
@jaredpar - re your comment:
Per https://docs.microsoft.com/en-us/dotnet/csharp/reference-semantics-with-value-types,
So it appears to contradict your suggestion. Am I misinterpreting? |
I actually think it's a bad idea to not require "in" on the call. The thread-safety issue means the caller should "acknowledge" the lack of thread-safety when passing using "in" versus non-"in". Without requiring "in" on the call, the call looks like any historical call which is thread-safe insofar as the struct arguments. But yet if the method signature has "in" then the call isn't thread-safe even through the calling code lures developers into seeing it as being safe. Requiring "in" also means the method signature cannot be changed between thread-safe (no-"in") and thread-unsafe ("in") without triggering compiler errors on the calling code and requiring a programmer to recognize the thread safety issue. By far "mostly" this is a concern when changing non-"in" to "in" which seems to be most likely to encounter thread-safety issues for "good" code. I'm also envisioning "possibly" it also being a concern for code in which the called method waits for a semaphore to release before accessing updated values in a struct ref passed via "in". This certainly sounds on the surface like maybe a pretty distasteful coding practice, but the language specs says it should work. |
@udlose that sentence is confusingly worded. The difference is subtle and as I'm trying to type up the explanation I'm beginning to understand why it's hard to construct a single sentence that adequately explains the problem. A longer way of saying it is:
Or in code terms // Okay
void M(int i) { ... }
void M(ref int i) { ... }
// Not Okay
void G(in int i) { ... }
void G(ref int i) { ... } |
Why would either of these necessarily be more thread safe than the other? For that matter what do you mean by thread-safe here: threading has far too many scenarios to be covered by a single term. Except of course immutable. |
@jaredpar - hah! that's cheating :) |
By "thread safe" I mean "safe for use in multithreading scenarios". When a struct is passed-by-copy (as is the case today without "in"), the callee is insensitive to whether the original struct gets subsequently modified by another thread. However when the struct is passed by reference (even using "in"), the original struct is being referenced by the callee. Thus if the original struct is modified by another thread, the callee will "see" the change. |
Which multithreading scenarios? As noted there are too many, many of which have contradictory correctness rules, to describe in a single all up term. |
@BillWagner - Thank you. oh, to be a fly on the wall! It'd be great to hear those discussions. |
Basically any potential multithreading scenarios in which the struct being passed is being accessed by multiple threads. If the struct is passed-by-copy (historical), then the callee doesn't care because it has its own local stack copy. If the struct is passed by-ref ("in"), then the callee does care whether multiple threads are accessing the original struct. If a semaphore or other lock is protecting the struct, the callee need not participate in the lock when the struct is passed-by-value. However when the struct is pass by reference, the callee may need to participate in the lock if there is concern about potentially reading a partially updated struct. Note these concerns apply when passing by "ref" also, except "ref" means also caring about writes (not just reads) to the struct if other threads can access it. |
Well, I'm not sure about it; the method can call a callback which can mutate the original struct. Somthing like this:
Replacing edit by @BillWagner to attribute quote to original author (see matching edit on issue description) |
This is only true for the data directly in the struct though. Data which is indirectly referenced, via say a field to a class object, is just as vulnerable in either scenario. This is also not applicable when the struct itself is |
@vladd - Touche! Good catch. It is a little less "nefarious" as far as a developer trying to understand the possibilities since the callee is at least participating in that case. But it is certainly another entirely legitimate "difference". I'm not convinced the not requiring "in" on the call may have been fully explored. I can understand the "friendliness" of not requiring it, but I think there is way too much to be said for making the understanding between the caller and callee more explicit. Particularly when it comes to a future programmer trying to trace through older code -- or even me trying to trace through my code from last week :-). |
@jaredpar -- All true. The debate really isn't about when to use "in" or not. The debate is only regarding clarity (or guidelines) for when a developer decides to use one or the other. The class reference in the struct is true both with and without "in", just like if the class reference was passed as its own parameter. This has more to do with the struct members that would be "safe" during pass-by-copy which become potentially "unsafe" during pass-by-ref. |
@kburgoyne what you are describing is basically aliasing of variables. Multithreading does not need to be involved to observe it. Consider examples:
Also note that if X is readonly, then the method is called on a copy. Then you can observe that there is no aliasing. Aliasing has complicated, but very well defined behavior. Code that takes advantage of aliasing tend to be more subtle - that is correct. However, the scenarios where you can change one variable and see another seemingly unrelated variable changing its value are not very new. Specifying Most users though don`t write concurrent/racey code and do not rely on aliasing to a degree that it cannot be traced and becomes source of bugs. |
@VSadov -- I think we're getting sidetracked debating multi-threading when it is nothing more than one example of the situation. The underlying issue is that using "in" versus not using "in" does have potential side effects and switching from non-"in" to "in" is potentially a "breaking change" -- which is where this thread got started. My assumption has been that at a minimum, documentation should point this out. Particularly for developers who may not be used to thinking about the details of what's happening at the machine level. There is also a significant "it's safer" argument to be made for requiring "in" on the call. Lacking that (which I find to be a very bad idea), there is going to need to be some pretty involved documentation behind spec'ing how the compiler selects between " |
@jaredpar How do you call the non-
Obviously, if Edit: discussed previously at dotnet/csharplang#945 (and the basic answer in C# 7.2 is "you can't"). |
Hopefully, I'm stating the obvious here....I think that writing code that is explicit and clearly states its intention is of the highest importance. Ambiguity in intention is dangerous. If it was designed to be optional to avoid it being a breaking change, I'd argue that is the wrong reason. Being clear in intention, especially when a new language feature is added, should be considered more important. I'd prefer it to be breaking so that if my devs go sprinkling IMO, |
This is the issue tracking support for |
@jaredpar - so has it been decided on whether or not |
It is not required. The reason for adding In the case of This is not something that was decided with the
Our decision around |
Which means at the moment you cannot add an overload which accepts an in parameter to an existing class because doing so breaks source level compatibility. given assembly A and B: namespace A
{
public class Ca
{
public void M(int i) => Console.WriteLine(i);
}
} namespace B
{
public class Cb
{
public void N()
{
var i = 0;
new A.Ca().M(i);
}
}
}
|
Another versioning question that I didn't see covered above: Is it a breaking change to add In CoreFX we’ve shipped: public partial struct Vector3 : System.IEquatable<System.Numerics.Vector3>, System.IFormattable
{
public static System.Numerics.Vector3 UnitX { get { throw null; } }
public static System.Numerics.Vector3 UnitY { get { throw null; } }
public static System.Numerics.Vector3 UnitZ { get { throw null; } }
public static System.Numerics.Vector3 Zero { get { throw null; } }
} Is it possible to add |
adding Which is unfortunate, since |
Same can be said about |
From @jskeet via LiveFyre:
It would be really nice to see a section on versioning concerns here. Is changing "public struct" to "readonly public struct" a breaking change? Is adding the "in" modifier to a method parameter a breaking change? I'll investigate for myself, but it would be great to see that information here.
More discussion on the topic:
From @kburgoyne
Since "in" means the callee is using a reference to the caller's struct rather than a copy of the caller's struct, there is the potential for it to break code. The key is whether there is any concern for the caller's copy to be changed while the callee is holding a ref to it. "in" really only says the callee won't/can't change the struct. It doesn't say the struct can't be changed elsewhere (by the caller or anything else that can access it). Obviously we're really talking multitasking/multithreading. In purely single threading nothing else has a chance to be executing while the callee is executing, so nothing else has the opportunity to change the struct.
More @jskeet
It feels like more than "potential" to break code - I suspect it's simply not binary compatible, in the same way that changing
Foo(int x)
toFoo(ref int x)
isn't binary compatible. I suspect this will limit where it can be used for public library projects :(/cc @VSadov @jaredpar
The text was updated successfully, but these errors were encountered: