식 사용 - 비파괴적 변경은 수정된 속성을 사용하여 새 개체를 생성

with 식은 지정된 속성과 필드가 수정된 피연산자의 복사본을 생성합니다. 개체 이니셜라이저 구문을 사용하여 수정할 멤버와 새 값을 지정합니다.

using System;

public class WithExpressionBasicExample
{
    public record NamedPoint(string Name, int X, int Y);

    public static void Main()
    {
        var p1 = new NamedPoint("A", 0, 0);
        Console.WriteLine($"{nameof(p1)}: {p1}");  // output: p1: NamedPoint { Name = A, X = 0, Y = 0 }
        
        var p2 = p1 with { Name = "B", X = 5 };
        Console.WriteLine($"{nameof(p2)}: {p2}");  // output: p2: NamedPoint { Name = B, X = 5, Y = 0 }
        
        var p3 = p1 with 
            { 
                Name = "C", 
                Y = 4 
            };
        Console.WriteLine($"{nameof(p3)}: {p3}");  // output: p3: NamedPoint { Name = C, X = 0, Y = 4 }

        Console.WriteLine($"{nameof(p1)}: {p1}");  // output: p1: NamedPoint { Name = A, X = 0, Y = 0 }

        var apples = new { Item = "Apples", Price = 1.19m };
        Console.WriteLine($"Original: {apples}");  // output: Original: { Item = Apples, Price = 1.19 }
        var saleApples = apples with { Price = 0.79m };
        Console.WriteLine($"Sale: {saleApples}");  // output: Sale: { Item = Apples, Price = 0.79 }
    }
}

with 식의 왼쪽 피연산자는 레코드 종류일 수 있습니다. C# 10부터 with 식의 왼쪽 피연산자가 구조체 형식 또는 무명 형식일 수도 있습니다.

다음 예제와 같이 with 식의 결과는 식의 피연산자와 동일한 런타임 형식입니다.

using System;

public class InheritanceExample
{
    public record Point(int X, int Y);
    public record NamedPoint(string Name, int X, int Y) : Point(X, Y);

    public static void Main()
    {
        Point p1 = new NamedPoint("A", 0, 0);
        Point p2 = p1 with { X = 5, Y = 3 };
        Console.WriteLine(p2 is NamedPoint);  // output: True
        Console.WriteLine(p2);  // output: NamedPoint { X = 5, Y = 3, Name = A }
    }
}

참조 형식 멤버인 경우 피연산자가 복사될 때 멤버 인스턴스에 대한 참조만 복사됩니다. 복사본과 원본 피연산자 모두 동일한 참조 형식 인스턴스에 액세스할 수 있습니다. 다음 예제에서는 해당 동작을 보여줍니다.

using System;
using System.Collections.Generic;

public class ExampleWithReferenceType
{
    public record TaggedNumber(int Number, List<string> Tags)
    {
        public string PrintTags() => string.Join(", ", Tags);
    }

    public static void Main()
    {
        var original = new TaggedNumber(1, new List<string> { "A", "B" });

        var copy = original with { Number = 2 };
        Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
        // output: Tags of copy: A, B

        original.Tags.Add("C");
        Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
        // output: Tags of copy: A, B, C
    }
}

사용자 지정 복사 의미 체계

모든 레코드 클래스 형식에는 복사 생성자가 있습니다. 복사 생성자는 레코드 형식의 단일 매개 변수를 사용합니다. 또한 인수의 상태를 새 레코드 인스턴스로 복사합니다. with 식을 평가할 때 복사 생성자가 호출되어 원본 레코드를 기반으로 새 레코드 인스턴스를 인스턴스화합니다. 그런 다음, 지정된 수정 사항에 따라 새 인스턴스가 업데이트됩니다. 기본적으로 복사 생성자는 암시적으로, 컴파일러에서 생성합니다. 레코드 복사 의미 체계를 사용자 지정해야 하는 경우 원하는 동작으로 복사 생성자를 명시적으로 선언합니다. 다음 예제에서는 명시적 복사 생성자를 사용하여 위의 예제를 업데이트합니다. 새 복사 동작은 레코드가 복사될 때 목록 참조 대신 목록 항목을 복사하는 것입니다.

using System;
using System.Collections.Generic;

public class UserDefinedCopyConstructorExample
{
    public record TaggedNumber(int Number, List<string> Tags)
    {
        protected TaggedNumber(TaggedNumber original)
        {
            Number = original.Number;
            Tags = new List<string>(original.Tags);
        }

        public string PrintTags() => string.Join(", ", Tags);
    }

    public static void Main()
    {
        var original = new TaggedNumber(1, new List<string> { "A", "B" });

        var copy = original with { Number = 2 };
        Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
        // output: Tags of copy: A, B

        original.Tags.Add("C");
        Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
        // output: Tags of copy: A, B
    }
}

구조체 형식에 대한 복사 의미 체계는 사용자 지정할 수 없습니다.

C# 언어 사양

자세한 내용은 레코드 기능 제안 노트의 다음 섹션을 참조하세요.

참고 항목