From 76f4e744d714a8a694e640da3e4c3f08af8e664e Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Tue, 15 Sep 2026 15:18:59 +0200 Subject: [PATCH] How to clone a list: retarget net10.0, collection expression and GetRange, remove broken ToppingsList clone Move both projects from net7.0 to net10.0 and update the test stack (xunit 2.9.3, runner 4.0.0, Test.Sdk 18.10.1, coverlet 10.0.1, FluentAssertions 7.2.2). Remove ToppingsList and its test: its Clone() called MemberwiseClone(), which copies the private backing array by reference, so the clone shared storage with the original and was not a clone at all. Add a collection-expression clone and a GetRange clone with tests, build the copy-constructor deep copy as a one-line projection, and add a test proving the projected clone keeps its toppings after the original is cleared. Fix the ConverAll typo in the console output, make Pizza.Name and Pizza.Toppings required with [SetsRequiredMembers] on the copy constructor, and use First instead of FirstOrDefault. The folder now builds with 0 warnings and runs 11 of 11 tests. --- .../HowToCloneAList.Tests.csproj | 12 ++++---- .../HowToCloneAList.Tests/PizzaTests.cs | 28 +++++++++++++++++-- .../HowToCloneAList.Tests/Tests.cs | 22 +++++++++++++++ .../ToppingsListTests.cs | 22 --------------- .../HowToCloneAList/HowToCloneAList.csproj | 2 +- .../HowToCloneAList/HowToCloneAList/Pizza.cs | 9 ++++-- .../HowToCloneAList/Program.cs | 23 +++++---------- .../HowToCloneAList/ToppingsList.cs | 10 ------- 8 files changed, 68 insertions(+), 60 deletions(-) delete mode 100644 collections-lists/HowToCloneAList/HowToCloneAList.Tests/ToppingsListTests.cs delete mode 100644 collections-lists/HowToCloneAList/HowToCloneAList/ToppingsList.cs diff --git a/collections-lists/HowToCloneAList/HowToCloneAList.Tests/HowToCloneAList.Tests.csproj b/collections-lists/HowToCloneAList/HowToCloneAList.Tests/HowToCloneAList.Tests.csproj index 22a057f9f9..0dd013a8ac 100644 --- a/collections-lists/HowToCloneAList/HowToCloneAList.Tests/HowToCloneAList.Tests.csproj +++ b/collections-lists/HowToCloneAList/HowToCloneAList.Tests/HowToCloneAList.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 enable enable @@ -9,14 +9,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/collections-lists/HowToCloneAList/HowToCloneAList.Tests/PizzaTests.cs b/collections-lists/HowToCloneAList/HowToCloneAList.Tests/PizzaTests.cs index 8105f3b552..2d6d22c924 100644 --- a/collections-lists/HowToCloneAList/HowToCloneAList.Tests/PizzaTests.cs +++ b/collections-lists/HowToCloneAList/HowToCloneAList.Tests/PizzaTests.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; namespace HowToCloneAList.Tests { @@ -60,5 +60,29 @@ public void GivenAValidPizza_WhenToStringMethodIsInvoked_ThenToStringMethodMetho expectedOutput.Should().Be(margherita.ToString()); } + + [Fact] + public void GivenAListOfPizzas_WhenProjectedThroughTheCopyConstructor_ThenTheCloneKeepsItsToppings() + { + var pizzas = new List + { + new Pizza + { + Name = "Margherita", + Toppings = new List + { + "Mozzarella", + "Olive oil", + "Basil" + } + } + }; + + List clone = [.. pizzas.Select(p => new Pizza(p))]; + + pizzas[0].Toppings.Clear(); + + clone[0].Toppings.Should().HaveCount(3); + } } -} \ No newline at end of file +} diff --git a/collections-lists/HowToCloneAList/HowToCloneAList.Tests/Tests.cs b/collections-lists/HowToCloneAList/HowToCloneAList.Tests/Tests.cs index 783e150390..fec0ed60e7 100644 --- a/collections-lists/HowToCloneAList/HowToCloneAList.Tests/Tests.cs +++ b/collections-lists/HowToCloneAList/HowToCloneAList.Tests/Tests.cs @@ -56,5 +56,27 @@ public void GivenAValidList_WhenConvertAllMethodIsInvoked_ThenConvertAllMethodRe listClone.Should().BeEquivalentTo(list); } + + [Fact] + public void GivenAValidList_WhenACollectionExpressionIsUsed_ThenTheCollectionExpressionReturnsNewListInstance() + { + var list = new List { "one", "two", "three" }; + + List listClone = [.. list]; + + listClone.Should().BeEquivalentTo(list); + listClone.Should().NotBeSameAs(list); + } + + [Fact] + public void GivenAValidList_WhenGetRangeMethodIsInvoked_ThenGetRangeMethodReturnsNewListInstance() + { + var list = new List { "one", "two", "three" }; + + var listClone = list.GetRange(0, list.Count); + + listClone.Should().BeEquivalentTo(list); + listClone.Should().NotBeSameAs(list); + } } } diff --git a/collections-lists/HowToCloneAList/HowToCloneAList.Tests/ToppingsListTests.cs b/collections-lists/HowToCloneAList/HowToCloneAList.Tests/ToppingsListTests.cs deleted file mode 100644 index 8246bd5f05..0000000000 --- a/collections-lists/HowToCloneAList/HowToCloneAList.Tests/ToppingsListTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FluentAssertions; - -namespace HowToCloneAList.Tests -{ - public class ToppingsListTests - { - [Fact] - public void GivenAValidToppingsList_WhenCloneMethodIsInvoked_ThenCloneMethodReturnsNewToppingsListInstance() - { - var customToppingsList = new ToppingsList - { - "Mozzarella", - "Olive oil", - "Basil" - }; - - var customToppingsListClone = (ToppingsList)customToppingsList.Clone(); - - customToppingsListClone.Should().BeEquivalentTo(customToppingsList); - } - } -} diff --git a/collections-lists/HowToCloneAList/HowToCloneAList/HowToCloneAList.csproj b/collections-lists/HowToCloneAList/HowToCloneAList/HowToCloneAList.csproj index f02677bf64..dfb40caafc 100644 --- a/collections-lists/HowToCloneAList/HowToCloneAList/HowToCloneAList.csproj +++ b/collections-lists/HowToCloneAList/HowToCloneAList/HowToCloneAList.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net10.0 enable enable diff --git a/collections-lists/HowToCloneAList/HowToCloneAList/Pizza.cs b/collections-lists/HowToCloneAList/HowToCloneAList/Pizza.cs index e628c0ecb6..145fdb640b 100644 --- a/collections-lists/HowToCloneAList/HowToCloneAList/Pizza.cs +++ b/collections-lists/HowToCloneAList/HowToCloneAList/Pizza.cs @@ -1,4 +1,6 @@ -namespace HowToCloneAList +using System.Diagnostics.CodeAnalysis; + +namespace HowToCloneAList { public class Pizza : ICloneable { @@ -6,14 +8,15 @@ public Pizza() { } + [SetsRequiredMembers] public Pizza(Pizza pizza) { Name = pizza.Name; Toppings = pizza.Toppings.ToList(); } - public string Name { get; set; } - public List Toppings { get; set; } + public required string Name { get; set; } + public required List Toppings { get; set; } public object Clone() { diff --git a/collections-lists/HowToCloneAList/HowToCloneAList/Program.cs b/collections-lists/HowToCloneAList/HowToCloneAList/Program.cs index 8cccaddfbe..ccc2ecbd8e 100644 --- a/collections-lists/HowToCloneAList/HowToCloneAList/Program.cs +++ b/collections-lists/HowToCloneAList/HowToCloneAList/Program.cs @@ -24,22 +24,18 @@ static void Main(string[] args) var toppingsClonedWithConvertAll = toppings .ConvertAll(new Converter(x => x)); - var customToppingsList = new ToppingsList - { - "Mozzarella", - "Olive oil", - "Basil" - }; + List toppingsClonedWithCollectionExpression = [.. toppings]; - var toppingsClonedWithICloneable = (ToppingsList)customToppingsList.Clone(); + var toppingsClonedWithGetRange = toppings.GetRange(0, toppings.Count); Console.WriteLine("Original list: " + string.Join(", ", toppings)); Console.WriteLine("Cloned with Constructor: " + string.Join(", ", toppingsClonedWithConstructor)); Console.WriteLine("Cloned with CopyTo: " + string.Join(", ", toppingsClonedWithCopyTo)); Console.WriteLine("Cloned with AddRange: " + string.Join(", ", toppingsClonedWithAddRange)); Console.WriteLine("Cloned with ToList: " + string.Join(", ", toppingsClonedWithToList)); - Console.WriteLine("Cloned with ConverAll: " + string.Join(", ", toppingsClonedWithConvertAll)); - Console.WriteLine("Cloned with ICloneable: " + string.Join(", ", toppingsClonedWithICloneable)); + Console.WriteLine("Cloned with ConvertAll: " + string.Join(", ", toppingsClonedWithConvertAll)); + Console.WriteLine("Cloned with a collection expression: " + string.Join(", ", toppingsClonedWithCollectionExpression)); + Console.WriteLine("Cloned with GetRange: " + string.Join(", ", toppingsClonedWithGetRange)); var pizzas = new List { @@ -74,15 +70,10 @@ static void Main(string[] args) pizzasClonedWithICloneable.Add((Pizza)pizza.Clone()); } - var pizzasClonedWithCopyConstructor = new List(); - - foreach (var pizza in pizzas) - { - pizzasClonedWithCopyConstructor.Add(new Pizza(pizza)); - } + List pizzasClonedWithCopyConstructor = [.. pizzas.Select(p => new Pizza(p))]; var margherita = pizzas - .FirstOrDefault(x => x.Name == "Margherita"); + .First(x => x.Name == "Margherita"); margherita.Toppings.Clear(); diff --git a/collections-lists/HowToCloneAList/HowToCloneAList/ToppingsList.cs b/collections-lists/HowToCloneAList/HowToCloneAList/ToppingsList.cs deleted file mode 100644 index 68ca91ac83..0000000000 --- a/collections-lists/HowToCloneAList/HowToCloneAList/ToppingsList.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace HowToCloneAList -{ - public class ToppingsList : List, ICloneable - { - public object Clone() - { - return this.MemberwiseClone(); - } - } -}