class 기본 생성자를 어떻게 생각하시나요?

아래 링크의 댓글을 보면 C# 12에 추가된 class 기본 생성자에 부정적인 글들이 꽤 있습니다.

기본 생성자를 이용하면 코드가 꽤 줄어들기는 합니다.

class UserInfo(string name, string sex)
{
    public void Print()
    {
        Console.WriteLine($"{name}: {sex}");
    }
}

하지만 개인적으로 기본 생성자의 인자가 readonly가 아닌 선택은 불만입니다. 다음처럼,

...
    public void Touch()
    {
        name += "!";
        sex += "!";
    }
UserInfo user = new("dimoy", "남");
user.Print();
user.Touch();
user.Print();

값이 변경됩니다.

dimoy: 남
dimoy!: 남!

생성 인자 값이 기본 readonly가 아닌 것은 정말 아쉽지만… 코드가 간결해지는 것 만큼은 좋은데요, 여러분 의견은 어떠세요?


2개의 좋아요

class 기본 생성자 기능을 그대로 사용하면서

말씀대로 위를 만족 하고 싶을때 'record’를 사용하면 안되는 상황이나 이유가 있을지 궁금합니다 :slight_smile:

record UserInfo(string name, string sex)
{
        public void Print()
        {
            Console.WriteLine($"{name}: {sex}");
        }
        
        public void Touch()
        {
            name += "!";  // Error
            sex += "!";  // Error
        }
}
3개의 좋아요

원래 클래스를 구현할 때 GetHashCode나 Equals 메서드 같은 것을 구현하는게 나중에 HashTable이나 Dictionary 등에 데이터 타입을 결합해서 사용하는 경우를 고려해서 꼭 필요하긴 하지만 손이 많이 가는 작업인데, record를 쓰면 이 부분이 많이 줄어드는 효과가 있는 것으로 기억합니다.

클래스 생성자는 그래서 클래스이든 레코드이든 어디서나 쓸 수 있는게 맞고, 클래스 생성자를 안쓴다고 하면 아래처럼 속성으로만 남기되 immutable 속성으로 남기는 것도 가능한 것으로 알고 있습니다.

class UserInfo() {
    public string name { get; init; }
    public string sex { get; init; }
}

다만 with 오퍼레이터는 record 타입인 경우에만 쓸 수 있게 되어있네요!

var a = new UserInfo() { name = "a", sex = "male", };

// UserInfo 타입이 레코드 타입이 아니라서 아래 코드는 컴파일 실패가 발생합니다.
var b = a with { name = "b", sex = "female", };
3개의 좋아요

추후 서로간의 상속이 불가하고 record에 자동구현된 부분들이 동작에 의도치않은 차이를 발생시키기 때문에 class의 역할을 record가 아무생각없이 대체하기엔 꽤나 숙고가 필요합니다.
객체의 용도가 record에 너무 적합하고 코드가 드라마틱하게 감량되는게 아니라면 일단 class인게 맞다고 생각합니다.

member가 아니라 parameter라는 관점에서, 제가봐온 C#은 인자의 불변성을 보장 해준적이 없습니다.
인자 하나로 불변 프로퍼티 만들고 이것저것 알아서 다하는 record가 별종으로 새로튀어나온것인데, 원래있던 class에서 그러한 모든게 어떠한 명시 없이 자동구현되어선 안된다고 생각합니다.
자동 readonly처리는 미래에 기본생성자와 함수의 인자를 포괄하는 새로운 문법이 발명될때로 미뤄져야합니다. 머 하다못해 attribute라도 나오겠죠

4개의 좋아요

위의 예시에서는 없습니다. UserInfo가 레코드(엔터티) 성질이 있으니까요. (한마디로 예시를 잘못 골랐네요

상태를 변경할 수 있는 일반적인 클래스 인스턴스의 경우입니다.

1개의 좋아요

image

당장 이런 느낌의 소스제너레이터를 개발한다면 좀 해소가 되지않을까? 막 떠오르는데 일단 누가만들어줄때까지 참겠습니다.

3개의 좋아요

오! 소스 생성기를 이용할 수 있겠군요. partial이 붙는다는 것이 좀 아쉽지만… 아이디어 멋집니다!

image

4개의 좋아요

저도 이 의견에 동의합니다.

자동화된 불변성은 record 에 맡겨 두고, class 의 가변성은 그대로 두는 것이 좋을 것 같다는 생각이 듭니다. 특히 엔티티를 위해서도 말이죠.

readonly 를 위해, 소스 생성기를 사용하는 것도 좋은 방법이지만, 프레임워크 차원에서 아래와 같이 파라미터를 위한 인조 필드(Synthesized fields) 생성을 억제하는 옵션을 제공하는 것이 좀 더 근원적인 해결책이 아닐까 합니다.

// .csproj

// true : 기존과 같음.
// false : 파라미터를 위한 인조 필드 생성 금지
<SynthesizeFieldsForPrimaryParameters>false</SynthesizeFieldsForPrimaryParameters>

이러한 기능이 제공되고, false 로 설정된 경우, 파라미터의 유효 범위는 initialization time 으로 축소되어, 필드의 초기화나 베이스 클래스의 기본 생성자 매개 변수 설정만 할 수 있게 됩니다.

class UserInfo(string name, string sex) : UserInfoBase (name)
{
    public UserInfo(string name, string sex, Guid id) : this (name, sex)
       => Id = id;

    public Guid Id { get; }
    private readonly string _sex = sex;
}

class UserInfoBase(string name)
{
    public string Name { get; set; } = name;
}

그런데, 이러한 기능은 이미 record 에 적용되어 있습니다.

record UserInfo(string Name, string Sex) : UserInfoBase(Name)
{
    public UserInfo(string name, string sex, Guid id) : this(name, sex)
       => Id = id;

    public Guid Id { get; } 
}

record UserInfoBase(string Name)
{
    public string NameMutable { get; set; } = Name;
    void Do() { Name = ""; } // 에러
}

그러나, record 만의 고유한 행태 특성(override 전파가 안됨, 복사 생성자 등)이 있기 때문에, 위의 class 코드와 완전히 같은 것은 아닙니다.

3개의 좋아요

오홋! 저도 이 얘기를 하고 싶었어용 ㅇㅅㅇ!

결국

// 1. primary contructor
public class Test(string name)
{
}

// 2. 그냥 클래스.
public class Test
{
    private string _name;
    public Test(name)
    {
        _name = name;
    }
}

이게 달라지면 더 문제일 거라는 생각이 듭니다.
1번일 때 readonly 가 적용되지 않아 문제라고 인식한다면
2번일 때에도 문제라고 얘기햬야하는데, 그렇지는 않으니까요.

게다가 readonly 가 필요하다면

public class Test(string name)
{
    private readonly string _name = name;
}

이 방법이 있으니까요.

primary constructor 가 강제 사항이 아니기 때문에
@BigSquare 님의 말씀처럼 원래 class 의 성격이 유지되어야 한다고 봐욤 ㅇㅅㅇ/

3개의 좋아요

여러 의견 감사핮니다. 이견이 없는것이 아니나 @BigSquare 님과 @Greg.Lee 님의 의견도 논리적이고 합리적입니다.

이견으로는 생성자에서 초기화 되는 동일 값(별도의 인스턴스 생성 없는)의 경우 개발환경에서 필드 값을 readonly로 추천 하고 기본 값을 readonly로 해도 그 반대를 필드 선언할 수 있기 때문에 문제가 없다는 입장이지만 class가 전통적으로 무엇이 되었든 기본값이 readonly가 아니기 때문에 납득이 됩니다.

다만 기본 생성자로 전달된 값은 변경이 되면서 클래스 안에서 전역적으로 접근된다는 점은 신경쓰이네요. 그것을 막는 유일한 방법은 ‘readonly int value = value;’ 방법밖에는 없습니다.

2개의 좋아요

기본 생성자를 readonly로 강제해서 씁니다.

Directory.Build.props

<Project>
  <PropertyGroup>
    <Nullable>enable</Nullable>
    <WarningsAsErrors>Nullable,MA0143</WarningsAsErrors>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Meziantou.Analyzer" Version="2.*">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>


3개의 좋아요

저는 처음에는 “아 이거 좀 별로인데?” 생각했다가 쓰고 나니까 “아 이거 생각보다 괜찮은데?” 생각해서 그냥 쓰고 있어요.

3개의 좋아요