Clone A C# List In Depth Review

Clone A List In Depth Review Banner Image

Introduction

When making a clone of a C# list it is important to understand the difference between a shallow and deep copy of a list and when they should be used.

Before we can get into details of the different ways of cloning a list, we need to go through the different data types and how each one is affected by the shallow and deep copy methods.

Data Types

Before getting into the shallow or deep copies. You need to have an understanding of the types and how they are stored in memory. The two of concern are value types and reference types.

Values Types

When value types are assigned data then the variable contains the instance of the type. This means that whenever we assign a variable of a value type it will always have a unique instance of the type.

For example, say we have two variables num1 and num2 and we assign num1 to num2. Since they are value types num1 and num2 will get their instance of type int and they are not linked to each other. So even if I change num2, it will not change num1.


int num1 = 4889;//Assign num1 first value
int num2 = num1;//Assign num1 to num2

Console.WriteLine($"Before Change num1:{num1},num2:{num2} ");
num2 = 30;//Assign a new value to num2, this will not change num1
Console.WriteLine($"After Change num1:{num1},num2:{num2} ");

Code Output
Before Change num1:4889,num2:4889
After Change num1:4889,num2:30

From this example, you can see that the value types are unique and can not affect one another by changing one even though I assigned one to the other.

Here is a list of some examples of the value types below. But they include numeric types, enums, and structs You can find the full list of value types here.

  1. int
  2. bool
  3. 3float
  4. double
  5. struct
Reference Types

Reference types are the reference point to the object. This differs from value types in that instead of pointing the value in memory, the reference type points to the address in memory where an instance of the object and its values reside. This means that in our code that two variables can be assigned memory references. Let's look at an example.

In this example, I created two car variables and assigned car1 to car2. What happens to change car2 and what would expect to happen?

Car car1 = new Car("SuperM", "M3");//Create New Car instance
Car car2 = car1;//assign car1 to car2

Console.WriteLine($"Before Change car1.Make:{car1.Make}, car1.Model:{car1.Model}");//Print result
Console.WriteLine($"Before Change car2.Make:{car2.Make}, car2.Model:{car2.Model}");//Print result
car2.Make = "ModelT";//Change car2 make name
car2.Model = "M9";//Change car2 model name
Console.WriteLine($"After Change car1.Make:{car1.Make}, car1.Model:{car1.Model}");//Print result
Console.WriteLine($"After Change car2.Make:{car2.Make}, car2.Model:{car2.Model}");//Print result

public class Car
{
    public Car(string make, string model)
    {
        Make = make;
        Model = model;
    }

    public string Make { get; set; }
    public string Model { get; set; }
}

Code Output
Before Change car1.Make:SuperM, car1.Model:M3
Before Change car2.Make:SuperM, car2.Model:M3
After Change car1.Make:ModelT, car1.Model:M9
After Change car2.Make:ModelT, car2.Model:M9

We see how this differs from value types. When assigning a reference to the different variables they are linked or in other words pointing to the same address in memory. Changing one then changes the other. This becomes the basis for our analysis of shallow and deep copying of a list because a list or any collection type is a reference type.

Shallow Copy

A shallow copy of a list is where each object in the list is copied from the old to the new so that the value types are assigned a new instance. This is known as a field-by-field copy.. For fields that are reference types, only the memory reference is copied. The advantage of this is that it is quick and saves memory consumption but not creating new objects but only copying the reference. The disadvantage is that if we need two separate lists and you want to change one of the list's contents without changing the other then a shallow copy will not work.

List Constructor Method

C# provides a built-in clone method through the List constructor. It is a shallow copy method so it is quick. I would recommend this method to mostly value types in the list. Let's look at some examples.

For this example, we'll create a list of transactions that in list set 1 are odd and use the list constructor to copy list set 1 to list set 2 then changes the values of list 2 to event numbers and see how the lists look after the change. Since the lists hold value types each item in the list will be its instance.

List Constructor Value Type Example
List<int> transactionsSet1 = new List<int>() { 1,3,5,7,9,11,13,15};//create a list of bank transations
List<int> transactionsSet2 = new List<int>(transactionsSet1);//Clone set1 to set2

Console.WriteLine($"Before Change set1 {string.Join(",",transactionsSet1)}");//Print result
Console.WriteLine($"Before Change set2 {string.Join(",",transactionsSet2)}");//Print result

transactionsSet2[0] = 2;//set item to new value
transactionsSet2[1] = 4;//set item to new value
transactionsSet2[2] = 6;//set item to new value
transactionsSet2[3] = 8;//set item to new value
transactionsSet2[4] = 10;//set item to new value
transactionsSet2[6] = 12;//set item to new value
transactionsSet2[7] = 14;//set item to new value

Console.WriteLine($"After Change set1 {string.Join(",", transactionsSet1)}");//Print result
Console.WriteLine($"After Change set2 {string.Join(",", transactionsSet2)}");//Print result

Code Output
Before Change set1 1,3,5,7,9,11,13,15
Before Change set2 1,3,5,7,9,11,13,15
After Change set1 1,3,5,7,9,11,13,15
After Change set2 2,4,6,8,10,11,12,14

As you can see from this output the two lists are distinct and separate because I'm only working with value types. Next, see how things change when we use reference types.

List Constructor Reference Type Example

Next, I created a Transaction class that holds the price and timestamp. Since when I create a class with fields it is considered a reference type. Since the List constructor method produces a shallow copy we would expect that when I do the same test as before that list1 would be affected too. Let's look at the example.

List<Transaction> transactionsSet1 = new List<Transaction>();
transactionsSet1.Add(new Transaction(1));//create new item
transactionsSet1.Add(new Transaction(3));//create new item
transactionsSet1.Add(new Transaction(5));//create new item

List<Transaction> transactionsSet2 = new List<Transaction>(transactionsSet1);

Console.WriteLine($"Before Change set1 {string.Join(",",transactionsSet1)}");//Print result
Console.WriteLine($"Before Change set2 {string.Join(",",transactionsSet2)}");//Print result
transactionsSet2[0].Price = 2;//set item to new value
transactionsSet2[1].Price = 4;//set item to new value
transactionsSet2[2].Price = 6;//set item to new value
Console.WriteLine($"After Change set1 {string.Join(",", transactionsSet1)}");//Print result
Console.WriteLine($"After Change set2 {string.Join(",", transactionsSet2)}");//Print result

class Transaction
{
    public Transaction(int price)
    {
        Price = price;
        TimeStamp = DateTime.Now;
    }

    public int Price { get; set; }
    public DateTime TimeStamp { get; set; }

    public override string? ToString()
    {
        return Price.ToString();
    }
}

Code Output
Before Change set1 1,3,5
Before Change set2 1,3,5
After Change set1 2,4,6
After Change set2 2,4,6

This confirms that list 2 is indeed a shallow copy of list 1 because we see that both lits are changed when we change one. Next, we'll test the speed of the method.

List Constructor Speed Test

For this test, we use the following parameters to test all the clone methods to compare the speed of each. In general, I expect shallow copy methods to be faster than deep ones because a shallow copy isn't creating a new object but only creating a new reference that points to the same object in memory.

10000 number in the list

25000 number times the method is called

10 number of tests

So one test is completed when 10 number times the method is called is completed.

using System.Diagnostics;
int numberOfTests = 10;//Number of tests 
List<double> testSpeedList = new List<double>();

for (int i = 0; i < numberOfTests; i++)
{
    testSpeedList.Add(ListConstructorMethodSpeedTest());
}
Console.WriteLine($"List Constructor Method Average speed:{Math.Round(testSpeedList.Average())}ms, In {numberOfTests} tests");

double ListConstructorMethodSpeedTest()
{

    int numberOfFunctionCalls = 25000;//Number of function calls made
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();//Start the Stopwatch timer
    for (int i = 0; i < numberOfFunctionCalls; i++)
    {
        stopwatch.Stop();
        List<Transaction> transactionList1 = GetInitialTransactionList();//Get intial random generated list
        stopwatch.Start();
        List<Transaction> transactionList2 = new List<Transaction>(transactionList1);//Use list constructor clone method
    }
    stopwatch.Stop();//Stop the Stopwatch timer
    Console.WriteLine($"Function calls:{numberOfFunctionCalls}, In {stopwatch.Elapsed.Minutes}m {stopwatch.Elapsed.Seconds}s {stopwatch.Elapsed.Milliseconds}ms");
    return stopwatch.Elapsed.TotalMilliseconds;
}
Supporting Code
List<Transaction> GetInitialTransactionList()
{
    List<Transaction> carList = new List<Transaction>();//Create new Car List as empty
    int numberOfObjectsToCreate = 10000;
    int maxTransactionNumber = 100000;
    for (int i = 0; i < numberOfObjectsToCreate; i++)
    {
        carList.Add(new Transaction(GetRandomInt(maxTransactionNumber)));//Add a new Car to the list
    }
    return carList;
}

int GetRandomInt(int maxNumber)
{
    Random random = new Random();//Create Random class
    int randomInt = random.Next(maxNumber);//Get a random number between 0 and the maxnumber
    return randomInt;
}

class Transaction
{
    public Transaction(int price)
    {
        Price = price;
        TimeStamp = DateTime.Now;
    }

    public int Price { get; set; }
    public DateTime TimeStamp { get; set; }

    public override string? ToString()
    {
        return Price.ToString();
    }
}
Code Output
Function calls:25000, In 0m 0s 82ms
Function calls:25000, In 0m 0s 80ms
Function calls:25000, In 0m 0s 80ms
Function calls:25000, In 0m 0s 88ms
Function calls:25000, In 0m 0s 82ms
Function calls:25000, In 0m 0s 80ms
Function calls:25000, In 0m 0s 85ms
Function calls:25000, In 0m 0s 84ms
Function calls:25000, In 0m 0s 101ms
Function calls:25000, In 0m 0s 109ms
List Constructor Method Average speed:87ms, In 10 tests

As we can see from this test that the List constructor is fast at an average of 87ms. Next, we'll look at the memory consumption of the new list.

List Constructor Memory Test

To look at memory used by the newly created list we're going to use a method called GC.GetTotalMemory to get the memory usage before and after the list creation.

Parameters of the Test

  1. 1000000 objects in the list
  2. 100000 max value of int in the list
List<Transaction> transactionsSet1 = GetInitialTransactionList();
long memoryUsageBefore = GC.GetTotalMemory(false);//Get memory size before test
List<Transaction> transactionsSet2 = new List<Transaction>(transactionsSet1);//Using List Constructor Clone Method
long memoryUsageAfter = GC.GetTotalMemory(false);//Get memory size after test
long memroyInBytes = memoryUsageAfter - memoryUsageBefore;//Get total memory usage by the test in bytes
double memoryInMB = Math.Round(((double)memroyInBytes) / (1024.0 * 1024.0), 2);//Convert bytes to MB
Console.WriteLine($"Difference In Memory is { memoryInMB} MB");//Print result
Supporting Code
List<Transaction> GetInitialTransactionList()
{
    List<Transaction> carList = new List<Transaction>();//Create new Car List as empty
    int numberOfObjectsToCreate = 1000000;
    int maxTransactionNumber = 100000;
    for (int i = 0; i < numberOfObjectsToCreate; i++)
    {
        carList.Add(new Transaction(GetRandomInt(maxTransactionNumber)));//Add a new Car to the list
    }
    return carList;

}

int GetRandomInt(int maxNumber)
{
    Random random = new Random();//Create Random class
    int randomInt = random.Next(maxNumber);//Get a random number between 0 and the maxnumber
    return randomInt;
}
Code Output
The difference In Memory is 7.63 MB

The memory usage from the list is about 8 MB which is pretty small for a list the size of 1 million objects. This is what we expect to get a good trade-off in performance and memory consumption.

### ToList Method

Another method we can use to get a shallow copy is to use the ToList method. [ It is a LINQ method that is often used for forcing a query to immediately execute.](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.tolist?view=net-7.0) But we can use it to clone a list as well.

##### ToLIst Reference Type Example

Since a value type example of this method would be redundant because value types will always behave the same in shallow and deep copies. I will not repeat that example for the rest of the methods. Instead, I will quickly verify that this is a shallow copy metho by the a reference type example.

```cs

List<Transaction> transactionsSet1 = new List<Transaction>();
transactionsSet1.Add(new Transaction(1));//create new item
transactionsSet1.Add(new Transaction(3));//create new item
transactionsSet1.Add(new Transaction(5));//create new item

List<Transaction> transactionsSet2 = transactionsSet1.ToList();//Using ToList Clone Method

Console.WriteLine($"Before Change set1 {string.Join(",", transactionsSet1)}");//Print result
Console.WriteLine($"Before Change set2 {string.Join(",", transactionsSet2)}");//Print result
transactionsSet2[0].Price = 2;//set item to new value
transactionsSet2[1].Price = 4;//set item to new value
transactionsSet2[2].Price = 6;//set item to new value
Console.WriteLine($"After Change set1 {string.Join(",", transactionsSet1)}");//Print result
Console.WriteLine($"After Change set2 {string.Join(",", transactionsSet2)}");//Print result

Code Output
Before Change set1 1,3,5
Before Change set2 1,3,5
After Change set1 2,4,6
After Change set2 2,4,6

This verifies that this is a shallow copy method since both lists were modified when I only modified one of them.

ToList Speed Test

I will continue the speed test used in the previous example and will only show the code differences below for this current test.

using System.Diagnostics;
int numberOfTests = 10;//Number of tests 
List<double> testSpeedList = new List<double>();

for (int i = 0; i < numberOfTests; i++)
{
    testSpeedList.Add(ToListMethodSpeedTest());
}
Console.WriteLine($"ToList Method Average speed:{Math.Round(testSpeedList.Average())}ms, In {numberOfTests} tests");

double ToListMethodSpeedTest()
{

    int numberOfFunctionCalls = 25000;//Number of function calls made
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();//Start the Stopwatch timer
    for (int i = 0; i < numberOfFunctionCalls; i++)
    {
        stopwatch.Stop();
        List<Transaction> transactionList1 = GetInitialTransactionList();//Get intial random generated list
        stopwatch.Start();
        List<Transaction> transactionList2 = transactionList1.ToList();//Use ToList method
    }
    stopwatch.Stop();//Stop the Stopwatch timer
    Console.WriteLine($"Function calls:{numberOfFunctionCalls}, In {stopwatch.Elapsed.Minutes}m {stopwatch.Elapsed.Seconds}s {stopwatch.Elapsed.Milliseconds}ms");
    return stopwatch.Elapsed.TotalMilliseconds;
}
Code Output
Function calls:25000, In 0m 0s 109ms
Function calls:25000, In 0m 0s 109ms
Function calls:25000, In 0m 0s 106ms
Function calls:25000, In 0m 0s 112ms
Function calls:25000, In 0m 0s 109ms
Function calls:25000, In 0m 0s 132ms
Function calls:25000, In 0m 0s 130ms
Function calls:25000, In 0m 0s 118ms
Function calls:25000, In 0m 0s 114ms
Function calls:25000, In 0m 0s 109ms
ToList Method Average speed:115ms, In 10 tests

With an average speed of 115ms, we can see that this method is slower than the List constructor method for doing the same thing.

ToList Method Memory Test

We'll test the ToList with the same parameters as the previous example. Below is the change in code.

List<Transaction> transactionsSet1 = GetInitialTransactionList();
long memoryUsageBefore = GC.GetTotalMemory(false);//Get memory size before test
List<Transaction> transactionsSet2 = transactionsSet1.ToList();//Using ToList Clone Method
long memoryUsageAfter = GC.GetTotalMemory(false);//Get memory size after test
long memroyInBytes = memoryUsageAfter - memoryUsageBefore;//Get total memory usage by the test in bytes
double memoryInMB = Math.Round(((double)memroyInBytes) / (1024.0 * 1024.0), 2);//Convert bytes to MB
Console.WriteLine($"Difference In Memory is { memoryInMB} MB");//Print result

Code Output
The difference In Memory is 7.63 MB

The memory consumption is the same as for the List constructor because both are shallow copy methods.

### Deep Copy

A deep copy is a value type that is the same as a shallow copy. In both cases, there's a new instance of the value of each variable. For example, if there's an int and it is assigned to another int then both will become separate instances and can not affect one another. The difference between deep and shallow clone copies is when dealing with reference types. While shallow copies only create new reference pointers that point to the same object. Deep copy creates a new reference pointer pointing to a new object altogether. This insures that a deep copy of a list even if changed will not affect it. When cloning a list and you do not want it to affect the list it was cloned from then you should use a deep clone copy. The trade-off comes from speed and memory consumption since a deep copy is creating a new object. With each newly created object is a memory and speed overhead.

### Constructor Copy Method

This is one way to create a deep clone copy of the list is the create a constructor that takes in the object and then creates a new object of every field in the object. Next, we'll need to loop through all the objects of list one and then clone all the objects from the list to list 2 using the constructor.

```cs
List<Transaction> transactionsSet1 = new List<Transaction>();
transactionsSet1.Add(new Transaction(1));//create new item
transactionsSet1.Add(new Transaction(3));//create new item
transactionsSet1.Add(new Transaction(5));//create new item

List<Transaction> transactionsSet2 = new List<Transaction>();
foreach (Transaction item in transactionsSet1)
{
    transactionsSet2.Add(new Transaction(item));
}

Console.WriteLine($"Before Change set1 {string.Join(",", transactionsSet1)}");//Print result
Console.WriteLine($"Before Change set2 {string.Join(",", transactionsSet2)}");//Print result
transactionsSet2[0].Price = 2;//set item to new value
transactionsSet2[1].Price = 4;//set item to new value
transactionsSet2[2].Price = 6;//set item to new value
Console.WriteLine($"After Change set1 {string.Join(",", transactionsSet1)}");//Print result
Console.WriteLine($"After Change set2 {string.Join(",", transactionsSet2)}");//Print result


class Transaction
{
    public Transaction(Transaction transaction)
    {
        if(transaction == null)
        {
            return;
        }
        //deep copy clone method
        Price = transaction.Price;//value type creates a new instance on the assignment
        TimeStamp = transaction.TimeStamp;//string creates a new instance on assignment
    }
    public Transaction(int price)
    {
        Price = price;
        TimeStamp = DateTime.Now;
    }

    public int Price { get; set; }
    public DateTime TimeStamp { get; set; }

    public override string? ToString()
    {
        return Price.ToString();
    }
}

Code Output
Before Change set1 1,3,5
Before Change set2 1,3,5
After Change set1 1,3,5
After Change set2 2,4,6

This returns exactly what we wanted. I'm able to modify one list of objects without affecting the other.

Constructor Copy Speed Test
using System.Diagnostics;
int numberOfTests = 10;//Number of tests 
List<double> testSpeedList = new List<double>();

for (int i = 0; i < numberOfTests; i++)
{
    testSpeedList.Add(ConstructorCopyMethodSpeedTest());
}
Console.WriteLine($"Constructor Copy Method Average speed:{Math.Round(testSpeedList.Average())}ms, In {numberOfTests} tests");

double ConstructorCopyMethodSpeedTest()
{

    int numberOfFunctionCalls = 25000;//Number of function calls made
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();//Start the Stopwatch timer
    for (int i = 0; i < numberOfFunctionCalls; i++)
    {
        stopwatch.Stop();
        List<Transaction> transactionList1 = GetInitialTransactionList();//Get initial random generated list
        stopwatch.Start();
        List<Transaction> transactionsSet2 = new List<Transaction>();
        foreach (Transaction item in transactionList1)
        {
            transactionsSet2.Add(new Transaction(item));
        }
    }
    stopwatch.Stop();//Stop the Stopwatch timer
    Console.WriteLine($"Function calls:{numberOfFunctionCalls}, In {stopwatch.Elapsed.Minutes}m {stopwatch.Elapsed.Seconds}s {stopwatch.Elapsed.Milliseconds}ms");
    return stopwatch.Elapsed.TotalMilliseconds;
}
Supporting Code
class Transaction
{
    public Transaction(Transaction transaction)
    {
        if (transaction == null)
        {
            return;
        }
        //deep copy method
        Price = transaction.Price;//value type creates a new instance on assignment
        TimeStamp = transaction.TimeStamp;//string creates a new instance on assignment
    }
    public Transaction(int price)
    {
        Price = price;
        TimeStamp = DateTime.Now;
    }

    public int Price { get; set; }
    public DateTime TimeStamp { get; set; }

    public override string? ToString()
    {
        return Price.ToString();
    }
}
Code Output
Function calls:25000, In 0m 19s 883ms
Function calls:25000, In 0m 19s 817ms
Function calls:25000, In 0m 19s 763ms
Function calls:25000, In 0m 20s 361ms
Function calls:25000, In 0m 20s 395ms
Function calls:25000, In 0m 20s 255ms
Function calls:25000, In 0m 19s 479ms
Function calls:25000, In 0m 19s 585ms
Function calls:25000, In 0m 19s 455ms
Function calls:25000, In 0m 19s 608ms
Constructor Copy Method Average speed:19861ms, In 10 tests

With an average speed of 19 seconds, the copy constructor is incredibly slow when the shallow copy methods take less than a second to complete the same test.

Constructor Copy Memory Test

I'll use the same test parameters as with the shallow copy tests to measure memory consumption.

List<Transaction> transactionsSet1 = GetInitialTransactionList();
long memoryUsageBefore = GC.GetTotalMemory(false);//Get memory size before test
List<Transaction> transactionsSet2 = new List<Transaction>();
foreach (Transaction item in transactionsSet1)
{
    transactionsSet2.Add(new Transaction(item));//create deep clone of object
}
long memoryUsageAfter = GC.GetTotalMemory(false);//Get memory size after test
long memroyInBytes = memoryUsageAfter - memoryUsageBefore;//Get total memory usage by the test in bytes
double memoryInMB = Math.Round(((double)memroyInBytes) / (1024.0 * 1024.0), 2);//Convert bytes to MB
Console.WriteLine($"Difference In Memory is { memoryInMB} MB");//Print result

class Transaction
{
    public Transaction(Transaction transaction)
    {
        if (transaction == null)
        {
            return;
        }
        //deep copy method
        Price = transaction.Price;//value type creates a new instance on the assignment
        TimeStamp = transaction.TimeStamp;//string creates a new instance on assignment
    }
    public Transaction(int price)
    {
        Price = price;
        TimeStamp = DateTime.Now;
    }

    public int Price { get; set; }
    public DateTime TimeStamp { get; set; }

    public override string? ToString()
    {
        return Price.ToString();
    }
}
Code Output
The difference In Memory is 34.62 MB

We can see that this is a much bigger size increase compared to a shallow copy. It is almost 5 times bigger than the same-sized list from the shallow copy.

### ICloneable Interface Method

This method requires extending the class with the ICloneable interface and then overloading the clone method. ICloneable depends on how it is implemented whether or not is a deep or shallow copy. Which is when you'll need to understand the different types and what time of clone you want to do. For this example, I'm using ICloneable to create a deep copy. Then we'll loop through the and call the clone method. Example below.

```cs


List<Transaction> transactionsSet1 = new List<Transaction>();
transactionsSet1.Add(new Transaction(1));//create new item
transactionsSet1.Add(new Transaction(3));//create new item
transactionsSet1.Add(new Transaction(5));//create new item

List<Transaction> transactionsSet2 = new List<Transaction>();

foreach (Transaction item in transactionsSet1)
{
    transactionsSet2.Add((Transaction)item.Clone());//Call clone for a deep copy of this
}

Console.WriteLine($"Before Change set1 {string.Join(",", transactionsSet1)}");//Print result
Console.WriteLine($"Before Change set2 {string.Join(",", transactionsSet2)}");//Print result
transactionsSet2[0].Price = 2;//set item to new value
transactionsSet2[1].Price = 4;//set item to new value
transactionsSet2[2].Price = 6;//set item to new value
Console.WriteLine($"Before Change set1 {string.Join(",", transactionsSet1)}");//Print result
Console.WriteLine($"Before Change set2 {string.Join(",", transactionsSet2)}");//Print result

class Transaction : ICloneable
{
    public Transaction()
    {

    }
    public Transaction(int price)
    {
        Price = price;
        TimeStamp = DateTime.Now;
    }

    public int Price { get; set; }
    public DateTime TimeStamp { get; set; }

    public object Clone()
    {
        Transaction transaction = new Transaction();
        transaction.Price = Price;
        transaction.TimeStamp = TimeStamp;
        return transaction;
    }

    public override string? ToString()
    {
        return Price.ToString();
    }
}


Code Output
Before Change set1 1,3,5
Before Change set2 1,3,5
After Change set1 1,3,5
After Change set2 2,4,6

As we can see this method also produce a deep copy of the list.

ICloneable Interface Method Speed Test

We'll conduct the same speed test as before in the other examples. Since this is a deep copy implementation, it is expected to be slower than a shallow copy method.


using System.Diagnostics;
int numberOfTests = 10;//Number of tests 
List<double> testSpeedList = new List<double>();

for (int i = 0; i < numberOfTests; i++)
{
    testSpeedList.Add(ICloneableInterfaceMethodSpeedTest());
}
Console.WriteLine($"ICloneable Interface  Method Average speed:{Math.Round(testSpeedList.Average())}ms, In {numberOfTests} tests");

double ICloneableInterfaceMethodSpeedTest()
{

    int numberOfFunctionCalls = 25000;//Number of function calls made
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();//Start the Stopwatch timer
    for (int i = 0; i < numberOfFunctionCalls; i++)
    {
        stopwatch.Stop();
        List<Transaction> transactionList1 = GetInitialTransactionList();//Get initial random generated list
        stopwatch.Start();
        List<Transaction> transactionsSet2 = new List<Transaction>();
        foreach (Transaction item in transactionList1)
        {
            transactionsSet2.Add((Transaction)item.Clone());//Call clone for a deep copy of this
        }
    }
    stopwatch.Stop();//Stop the Stopwatch timer
    Console.WriteLine($"Function calls:{numberOfFunctionCalls}, In {stopwatch.Elapsed.Minutes}m {stopwatch.Elapsed.Seconds}s {stopwatch.Elapsed.Milliseconds}ms");
    return stopwatch.Elapsed.TotalMilliseconds;
}
Supporting Code
class Transaction : ICloneable
{
    public Transaction()
    {

    }
    public Transaction(int price)
    {
        Price = price;
        TimeStamp = DateTime.Now;
    }

    public int Price { get; set; }
    public DateTime TimeStamp { get; set; }

    public object Clone()
    {
        Transaction transaction = new Transaction();
        transaction.Price = Price;
        transaction.TimeStamp = TimeStamp;
        return transaction;
    }

    public override string? ToString()
    {
        return Price.ToString();
    }
}
Code Output
Function calls:25000, In 0m 20s 3ms
Function calls:25000, In 0m 20s 62ms
Function calls:25000, In 0m 20s 144ms
Function calls:25000, In 0m 20s 8ms
Function calls:25000, In 0m 19s 995ms
Function calls:25000, In 0m 19s 990ms
Function calls:25000, In 0m 19s 983ms
Function calls:25000, In 0m 20s 46ms
Function calls:25000, In 0m 19s 933ms
Function calls:25000, In 0m 19s 901ms
ICloneable Interface  Method Average speed:20007ms, In 10 tests

This is very slow as expected but only slightly slower than the copy constructor method.

ICloneable Interface Method Memory Test

This method will be tested against 100 million objects to see what the memory consumption is before and after the deep copy is made.


List<Transaction> transactionsSet1 = GetInitialTransactionList();
long memoryUsageBefore = GC.GetTotalMemory(false);//Get memory size before test
List<Transaction> transactionsSet2 = new List<Transaction>();
foreach (Transaction item in transactionsSet1)
{
    transactionsSet2.Add((Transaction)item.Clone());//Call clone for a deep copy of this
}
long memoryUsageAfter = GC.GetTotalMemory(false);//Get memory size after test
long memroyInBytes = memoryUsageAfter - memoryUsageBefore;//Get total memory usage by the test in bytes
double memoryInMB = Math.Round(((double)memroyInBytes) / (1024.0 * 1024.0), 2);//Convert bytes to MB
Console.WriteLine($"Difference In Memory is { memoryInMB} MB");//Print result


class Transaction : ICloneable
{
    public Transaction()
    {

    }
    public Transaction(int price)
    {
        Price = price;
        TimeStamp = DateTime.Now;
    }

    public int Price { get; set; }
    public DateTime TimeStamp { get; set; }

    public object Clone()
    {
        Transaction transaction = new Transaction();
        transaction.Price = Price;
        transaction.TimeStamp = TimeStamp;
        return transaction;
    }

    public override string? ToString()
    {
        return Price.ToString();
    }
}


Code Output
The difference In Memory is 34.6 MB

This memory number is the same as the previous deep copy method and this is about 5 times what shallow copy would create.

### Conclusion

<Table header ="Overall Rank,Method,Clone Type,Speed,Memory Consumption" rows="1,List Constructor,Shallow Copy,87ms,7.63MB;2,ToList,Shallow Copy,115ms,7.63MB;3,Constructor Copy,Deep Copy,19861ms,34.62MB;4,ICloneable Interface,Deep Copy,20007ms,34.6MB;" />

Overall, List Constructor is the best method to clone a list. It is fast, easy to set up, and doesn't consume much memory. Use this if you are only working with value types and don't need a deep copy.

The second best method is the ToList method, it is also fast but requires the extra library of LINQ to set it up.

If you working with reference-type objects in the list then constructor copy is the best method to use. Even though it is slow and consumes more memory. it will be worth it to have a deep copy of your lists so that they are completely separate from each other.
Get Latest Updates
Comments Section