') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Make ValueTuple members that can be made readonly readonly by Neme12 · Pull Request #90773 · dotnet/runtime · GitHub
Skip to content

Make ValueTuple members that can be made readonly readonly - #90773

Closed
Neme12 wants to merge 3 commits into
dotnet:mainfrom
Neme12:valuetuple-readonly
Closed

Make ValueTuple members that can be made readonly readonly#90773
Neme12 wants to merge 3 commits into
dotnet:mainfrom
Neme12:valuetuple-readonly

Conversation

@Neme12

@Neme12Neme12 commented Aug 17, 2023

Copy link
Copy Markdown

I applied readonly to all methods except for GetHashCode and ToString, which can't be made readonly (or can be, but it would make copies of the items) because they forward to methods on the individual items that could potentially modify them.

@ghostghost added needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners community-contribution Indicates that the PR has been added by a community member labels Aug 17, 2023
@teo-tsirpanis

teo-tsirpanis commented Aug 17, 2023

Copy link
Copy Markdown
Contributor

@Neme12

Copy link
Copy Markdown
Author

Thanks.

@neon-sunset

neon-sunset commented Aug 18, 2023

Copy link
Copy Markdown
Contributor

Doesn't this break C# tuple contract? For example

// Valid codevartuple=(1,2);tuple.Item2=3;

@huoyaoyuan

Copy link
Copy Markdown
Member

Doesn't this break C# tuple contract? For example

Only the 0-ary ValueTuple is marked as readonly as a whole type. The members of others are still writable.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

Issue Details

I applied readonly to all methods except for GetHashCode and ToString, which can't be made readonly (or can be, but it would make copies of the items) because they forward to methods on the individual items that could potentially modify them.

Author:Neme12
Assignees:-
Labels:

area-System.Runtime, community-contribution, needs-area-label

Milestone:-

@jkotasjkotas removed the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Aug 18, 2023

@jozkeejozkee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this.
How does GetHashCode() and ToString() create a defensive copy? sharplab.io shows identical asm.

Also, there have been other issues filed where this is being discussed. Just referencing them here.

@adamsitnik

Copy link
Copy Markdown
Member

@dotnet/fxdc does applying readonly to a type and/or method requires going through API review (removing it would cause a breaking change)?

@stephentoub@jkotas what is our current strategy for similar changes? I saw #46675 (comment)

annotate methods on non-readonly structs which do not and will never mutate the state of the instance

And I believe that most of the methods marked as readonly in this PR (Equals, indexer, GetHashCode) will never mutate. But it's not very clear to me what we would gain from it.

@stephentoub

Copy link
Copy Markdown
Member

does applying readonly to a type and/or method requires going through API review (removing it would cause a breaking change)?

We've generally preferred to do so, yes, ideally in bulk.

it's not very clear to me what we would gain from it

The main consumer gain for us marking something readonly is it can avoid a defensive copy when a value is passed via in.

Let's say you have:

(long,long,long,long)value= ...;Foo(refvalue);
...void Foo(ref(long,long,long,long)data){data.GetHashCode();}

The compiler doesn't need to do anything special here around preventing mutation. But if instead the code was:

(long,long,long,long)value= ...;Foo(invalue);
...void Foo(in(long,long,long,long)data){data.GetHashCode();}

The in is promising that the caller's value won't be mutated, which means when compiling Foo, the compiler needs to ensure that data isn't mutated, but the compiler doesn't know what GetHashCode does... it could be writing to this, in which case it would be mutating data and thus value. So if GetHashCode isn't readonly (either directly or because the struct on which it's declared is readonly), the compiler needs to make a defensive copy, and it's compiled instead as if it were:

voidFoo(ref(long,long,long,long)data){(long,long,long,long)copy=data;copy.GetHashCode();}

Whether that has any actual performance impact depends on a variety of factors, e.g. if the method being called is inlined, there's a good chance the JIT will eliminate the copy.

}

int IStructuralComparable.CompareTo(object? other, IComparer comparer)
readonly int IStructuralComparable.CompareTo(object? other, IComparer comparer)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting readonly on explicit interface implementations isn't going to buy you anything. These can only be accessed via interface references and those don't have defensive copy semantics.

I'm actually a bit surprised we allow this syntax.

@tannergooding

Copy link
Copy Markdown
Member

The main consumer gain for us marking something readonly is it can avoid a defensive copy when a value is passed via in

The other notable case is static readonly (int, int) s_value = ...; and then doing s_value.Method() or similar.


That being said, this is all generic code and we're just trading one copy for another copy here.

That is, today, if you have readonly (int, int) value = ... and do say value.GetHashCode() you get a copy of the full value and then GetHashCode() is called on that copy.

If you instead mark GetHashCode() as readonly, then it cannot statically determine that Item1.GetHashCode() won't mutate and Roslyn will insert independent copies of both Item1 and Item2 before calling their own respective GetHashCode() methods.

In the case where all types are unmanaged, the JIT can optimize the former copy to a block copy, while it may needs to preserve the per item copies in the latter case. Which is better is ultimately scenario dependent, but I don't think this is something we can really "fix", since we can't actually remove the copies without unsafe code.

@adamsitnikadamsitnik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does applying readonly to a type and/or method requires going through API review (removing it would cause a breaking change)?

We've generally preferred to do so, yes, ideally in bulk.

In such a case I am going to convert this PR to a draft and create a new API proposal based on the ref file changes.

@adamsitnik
adamsitnik marked this pull request as draft November 3, 2023 17:15
@huoyaoyuan

Copy link
Copy Markdown
Member

Historically, #44640 and #44629 didn't go through API review.
This one should be really trivial though.

@ghostghost closed this Dec 7, 2023
@ghost

ghost commented Dec 7, 2023

Copy link
Copy Markdown

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@terrajobstterrajobst reopened this Dec 7, 2023
@terrajobst

Copy link
Copy Markdown
Contributor

Before we close this, is this intended to still be one?

From an API review standpoint, this affects public surface area and should normally go through API review but my care level is relatively low. Unless anyone on @dotnet/fxdc objects, I'm OK with letting these kind of changes be handled by PR review alone.

@ghostghost closed this Jan 6, 2024
@ghost

ghost commented Jan 6, 2024

Copy link
Copy Markdown

Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it.

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Feb 6, 2024
This pull request was closed.
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Runtimecommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

11 participants

@Neme12@teo-tsirpanis@neon-sunset@huoyaoyuan@adamsitnik@stephentoub@tannergooding@terrajobst@jaredpar@jozkee@jkotas