.NET에서 구조체의 기본 생성자를 정의 할 수없는 이유는 무엇입니까?


261

.NET에서 값 유형 (C # struct)은 매개 변수가없는 생성자를 가질 수 없습니다. 이 게시물 에 따르면 이것은 CLI 사양에 의해 요구됩니다. 모든 값 유형에 대해 기본 생성자가 (컴파일러에 의해) 생성되어 모든 멤버를 0으로 초기화합니다 (또는 null).

이러한 기본 생성자를 정의 할 수없는 이유는 무엇입니까?

한 가지 사소한 용도는 유리수입니다.

public struct Rational {
    private long numerator;
    private long denominator;

    public Rational(long num, long denom)
    { /* Todo: Find GCD etc. */ }

    public Rational(long num)
    {
        numerator = num;
        denominator = 1;
    }

    public Rational() // This is not allowed
    {
        numerator = 0;
        denominator = 1;
    }
}

현재 버전의 C #을 사용하면 기본 Rational이 0/0그리 좋지 않습니다.

추신 : 기본 매개 변수가 C # 4.0 에서이 문제를 해결하는 데 도움이됩니까? 또는 CLR 정의 기본 생성자가 호출됩니까?


Jon Skeet 가 대답했습니다.

당신의 모범을 사용하기 위해 누군가가했을 때 어떤 일이 일어나고 싶습니까?

 Rational[] fractions = new Rational[1000];

생성자를 1000 번 실행해야합니까?

물론, 이것이 기본 생성자를 처음에 쓴 이유입니다. 명시 적 기본 생성자가 정의되지 않은 경우 CLR은 기본 제로 생성자를 사용해야합니다 . 그렇게하면 사용한만큼만 지불합니다. 그런 다음 기본이 아닌 1000 개의 컨테이너를 Rational원하고 1000 구성을 최적화하려는 List<Rational>경우 배열 대신 오히려 사용합니다 .

내 생각에이 이유는 기본 생성자의 정의를 막기에 충분하지 않다.


3
+1도 비슷한 문제를 겪고 마침내 구조체를 클래스로 변환했습니다.
Dirk Vollmar

4
C # 4의 기본 매개 변수는을 사용하지 Rational()않고 매개 변수가없는 ctor를 호출 하기 때문에 도움이되지 않습니다 Rational(long num=0, long denom=1).
LaTeX

6
에 있습니다 C # 6.0 비주얼 스튜디오 2015와 함께 제공이 구조체에 대한 제로 매개 변수 인스턴스 생성자를 쓸 수됩니다. 그래서 new Rational()존재하는 경우 존재하지 않는 그러나 경우, 생성자를 호출 new Rational()에 해당 될 것입니다 default(Rational). 어쨌든 default(Rational)구조체의 "제로 값"(원하는 디자인의 "나쁜"숫자)을 원할 때 구문을 사용하는 것이 좋습니다 Rational. 값 유형의 기본값 T은 항상 default(T)입니다. 따라서 new Rational[1000]구조체 생성자를 호출하지 않습니다.
Jeppe Stig Nielsen

6
이 특정 문제를 해결하기 위해 denominator - 1구조체 안에 저장할 수 있습니다 . 기본값은 0/1이됩니다
miniBill

3
Then if I want a container of 1000 non-default Rationals (and want to optimize away the 1000 constructions) I will use a List<Rational> rather than an array.왜 배열이 구조체의 List에 대해 다른 생성자를 호출해야합니까?
mjwills

답변:


197

참고 : 아래 답변은 C # 6 이전에 오래 전에 작성 되었습니다. 이 기능 은 C # 6에 추가되지 않았습니다 .


편집 : Grauenwolf의 CLR에 대한 통찰력으로 인해 아래 답변을 편집했습니다.

CLR을 사용하면 값 형식에 매개 변수가없는 생성자가있을 수 있지만 C #에는 없습니다. 나는 이것이 생성자가 그렇지 않을 때 호출 될 것이라는 기대를 불러 일으킬 것이라고 생각합니다. 예를 들어 다음을 고려하십시오.

MyStruct[] foo = new MyStruct[1000];

CLR은 적절한 메모리를 할당하고 모두 제로화하여이 작업을 매우 효율적으로 수행 할 수 있습니다. MyStruct 생성자를 1000 번 실행해야한다면 효율성이 훨씬 떨어집니다. (사실, 그것은하지 않습니다 - 당신이 경우에 매개 변수없는 생성자가 당신이 배열을 만들 때이 실행되지 않습니다, 또는 초기화되지 않은 인스턴스 변수가있을 때.)

C #의 기본 규칙은 "모든 유형의 기본값은 초기화에 의존 할 수 없습니다"입니다. 이제 그들은 할 수 있었다 매개 변수가없는 생성자를 정의 할 수 있습니다,하지만 생성자는 모든 경우에 수행 될 필요는 없다 -하지만 더 혼란을 주도했을 것이다. (또는 적어도 논쟁의 여지가 있다고 생각합니다.)

편집 : 귀하의 예를 사용하기 위해 누군가가했을 때 어떻게되고 싶습니까?

Rational[] fractions = new Rational[1000];

생성자를 1000 번 실행해야합니까?

  • 그렇지 않다면, 1000 개의 유효하지 않은 합리적 결과를 얻게됩니다
  • 그렇다면 배열을 실제 값으로 채우려 고하면 많은 작업을 낭비했을 것입니다.

편집 : (질문이 조금 더 있습니다) 매개 변수가없는 생성자는 컴파일러에 의해 생성되지 않습니다. 이 밝혀 있지만,이 - 값 유형은 멀리 CLR에 관한 한 같은 생성자를 할 필요는 없습니다 는 일리노이을 작성하는 경우. new Guid()C #에서 " " 를 작성할 때 일반 생성자를 호출하면 얻을 수있는 것과 다른 IL을 방출합니다. 해당 측면에 대한 자세한 내용은 이 SO 질문 을 참조하십시오 .

나는 의심 매개 변수가없는 생성자와 프레임 워크의 모든 값 유형이 아니라는 것을. 의심 할 여지없이 NDepend가 충분히 훌륭하게 요구했는지 말해 줄 수 있습니다 ... C #이 금지한다는 사실은 아마도 그것이 나쁜 생각이라고 생각하기에 충분히 큰 힌트 일 것입니다.


9
간단한 설명 : C ++에서 struct와 class는 같은 동전의 양면에 불과했습니다. 유일한 차이점은 하나는 기본적으로 공개되었고 다른 하나는 비공개였습니다. .Net에서는 구조체와 클래스 사이에 훨씬 큰 차이가 있으므로 이해하는 것이 중요합니다.
Joel Coehoorn

38
@Joel :이 특정 제한을 실제로 설명하지는 않습니까?
Jon Skeet

6
CLR에서는 값 형식에 매개 변수가없는 생성자가있을 수 있습니다. 그리고 네, 배열의 모든 요소마다 실행됩니다. C #은 이것이 나쁜 생각이라고 생각하고 허용하지 않지만 .NET 언어를 작성할 수는 있습니다.
Jonathan Allen

2
죄송합니다. 다음과 약간 혼동됩니다. 구조체 대신 클래스 인 Rational[] fractions = new Rational[1000];경우 많은 작업을 낭비 합니까 Rational? 그렇다면 왜 클래스에 기본 ctor가 있습니까?
내 겨드랑이에 키스

5
@ FifaEarthCup2014 : "작업량 낭비"의 의미에 대해보다 구체적으로 설명해야합니다. 그러나 어느 쪽이든 생성자를 1000 번 호출하지는 않습니다. Rational클래스 라면 1000 개의 null 참조 배열로 끝납니다.
Jon Skeet

48

구조체는 값 형식이며 값 형식은 선언되는 즉시 기본값을 가져야합니다.

MyClass m;
MyStruct m2;

인스턴스화하지 않고 위와 같이 두 개의 필드를 선언하면 디버거를 중단하면 mnull이되지만 m2그렇지 않습니다. 이것을 감안할 때 매개 변수가없는 생성자는 의미가 없습니다. 실제로 구조체의 모든 생성자는 값을 할당하는 것입니다. 실제로 m2는 위의 예에서 아주 행복하게 사용될 수 있으며, 해당되는 경우 해당 메소드와 필드 및 속성을 조작 할 수 있습니다!


3
왜 누군가가 당신에게 투표했는지 모르겠습니다. 당신은 여기에 가장 정답 인 것 같습니다.
pipTheGeek

12
C ++의 동작은 형식에 기본 생성자가 있으면 명시 적 생성자없이 이러한 개체를 만들 때 사용됩니다. 이것은 C #에서 기본 생성자로 m2를 초기화하는 데 사용될 수 있었기 때문에이 답변이 도움이되지 않습니다.
Motti

3
onester : 선언 할 때 구조체가 자신의 생성자를 호출하지 않으려면 기본 생성자를 정의하지 마십시오! :) 그것은 Motti의 말입니다
Stefan Monov

8
@Tarik. 난 동의하지 않는다. 반대로, 매개 변수가없는 생성자는 완전한 의미를 갖습니다. "매트릭스"구조체를 만들려면 항상 항등 행렬을 기본값으로 사용하면 다른 방법으로 어떻게 할 수 있습니까?
Elo

1
확실히 나는 완전히 동의하지"사실 m2 아주 행복하게 사용할 수 있습니다 ..." . 아니라 그것은 이전의 C #으로 진정한되었을 수도 있지만 구조체를 선언 컴파일러 오류의 new그것은, 다음 회원 사용하려고
카이 우스 JARD

18

CLR에서 허용하지만 C #에서는 구조체에 기본 매개 변수없는 생성자가있을 수 없습니다. 그 이유는 값 유형의 경우 기본적으로 컴파일러가 기본 생성자를 생성하지 않으며 기본 생성자에 대한 호출도 생성하지 않기 때문입니다. 따라서 기본 생성자를 정의한 경우에도 호출되지 않으며 혼동 될뿐입니다.

이러한 문제를 피하기 위해 C # 컴파일러는 사용자가 기본 생성자를 정의 할 수 없습니다. 또한 기본 생성자를 생성하지 않으므로 필드를 정의 할 때 필드를 초기화 할 수 없습니다.

또는 가장 큰 이유는 구조가 값 유형이고 값 유형이 기본값으로 초기화되고 생성자가 초기화에 사용되기 때문입니다.

당신은 당신의 구조체를 인스턴스화 할 필요가 없습니다. new키워드로 . 대신 int처럼 작동합니다. 직접 액세스 할 수 있습니다.

구조체에는 명시적인 매개 변수없는 생성자가 포함될 수 없습니다. 구조 부재는 자동으로 기본값으로 초기화됩니다.

구조체의 기본 (매개 변수없는) 생성자는 예상치 못한 동작 인 0으로 끝나는 상태와 다른 값을 설정할 수 있습니다. 따라서 .NET 런타임은 구조체의 기본 생성자를 금지합니다.


이 답변은 지금까지 최고입니다. 결국 제한의 핵심은 MyStruct s;당신이 제공 한 기본 생성자를 호출하지 않는 놀라움을 피하는 것입니다.
키가

1
설명 주셔서 감사합니다. 따라서 개선해야 할 것은 컴파일러 부족 일 뿐이며, 매개 변수가없는 생성자를 금지해야하는 이론적 인 이유는 없습니다 (속성에만 액세스하도록 제한되는 즉시).
Elo

16

기본 "합리적"숫자를 초기화하고 반환하는 정적 속성을 만들 수 있습니다.

public static Rational One => new Rational(0, 1); 

그리고 그것을 다음과 같이 사용하십시오 :

var rat = Rational.One;

24
이 경우 Rational.Zero조금 덜 혼란 스러울 수 있습니다.
케빈

13

더 짧은 설명 :

C ++에서 struct와 class는 같은 동전의 양면에 불과했습니다. 유일한 차이점은 하나는 기본적으로 공개되었고 다른 하나는 비공개였습니다.

.NET 에서는 구조체와 클래스 사이에 훨씬 큰 차이가 있습니다. 가장 중요한 것은 구조체가 값 형식 의미를 제공하는 반면 클래스는 참조 형식 의미를 제공한다는 것입니다. 이 변경의 의미에 대해 생각하기 시작하면 설명하는 생성자 동작을 포함하여 다른 변경도 더 의미가 있습니다.


7
내가 얻지 못하는 값 대 참조 유형 분할에 의해 이것이 어떻게 암시되는지에 대해 좀 더 명시 적으로 설명해야합니다 ...
Motti

값 유형에는 기본값이 있습니다. 생성자를 정의하지 않아도 널이 아닙니다. 언뜻보기에 이것은 기본 생성자를 정의하는 것을 방해하지는 않지만이 기능을 내부적으로 사용하여 구조체에 대한 특정 가정을 만드는 프레임 워크입니다.
Joel Coehoorn

@ annakata : 다른 생성자는 아마도 Reflection과 관련된 일부 시나리오에서 유용 할 것입니다. 또한 매개 변수화 된 "새로운"제한 조건을 허용하도록 제네릭을 개선 한 경우이를 준수 할 수있는 구조체를 갖는 것이 유용합니다.
supercat

@ annakata C #에는 new생성자를 호출하기 위해 실제로 작성 해야하는 특별한 요구 사항이 있기 때문에 믿습니다 . C ++에서 생성자는 배열을 선언하거나 표시 할 때 숨겨진 방식으로 호출됩니다. C #에서 모든 것은 포인터이므로 null에서 시작하거나 구조체이거나 무언가에서 시작해야하지만 new... (배열 init와 같이) 쓸 수 없으면 강력한 C # 규칙을 위반합니다.
v.oddou

3

나는 내가 줄 늦은 솔루션과 동등한 것을 보지 못 했으므로 여기에 있습니다.

오프셋을 사용하여 기본값 0에서 원하는 값으로 값을 이동하십시오. 여기서는 필드에 직접 액세스하는 대신 속성을 사용해야합니다. (가능한 c # 7 기능을 사용하면 속성 범위 필드를 더 잘 정의하여 코드에서 직접 액세스되지 않도록 할 수 있습니다.)

이 솔루션은 값 유형 만있는 간단한 구조체 (ref 유형 또는 nullable 구조체 없음)에 적합합니다.

public struct Tempo
{
    const double DefaultBpm = 120;
    private double _bpm; // this field must not be modified other than with its property.

    public double BeatsPerMinute
    {
        get => _bpm + DefaultBpm;
        set => _bpm = value - DefaultBpm;
    }
}

이것은 다른 것보다 이 방법은 특별한 케이스하지만이 모든 범위에 대해 작동되는 오프셋 사용하지 않는,이 답변.

열거 형을 필드로 사용하는 예입니다.

public struct Difficaulty
{
    Easy,
    Medium,
    Hard
}

public struct Level
{
    const Difficaulty DefaultLevel = Difficaulty.Medium;
    private Difficaulty _level; // this field must not be modified other than with its property.

    public Difficaulty Difficaulty
    {
        get => _level + DefaultLevel;
        set => _level = value - DefaultLevel;
    }
}

내가 말했듯이 struct에 값 필드 만있는 경우 에도이 트릭이 모든 경우에 작동하지는 않을 수 있습니다. 그냥 검사하십시오. 그러나 당신은 일반적인 생각을 얻습니다.


이것은 내가 준 예에 대한 좋은 해결책이지만 실제로는 단지 예일뿐이었습니다. 질문은 일반적입니다.
Motti

2

특별한 경우입니다. 분자가 0이고 분모가 0이면 실제로 원하는 값을 갖는 것처럼 가장하십시오.


5
나는 개인적으로 내 수업 / 구조체가 이런 종류의 행동을 좋아하지 않을 것입니다. 조용히 실패 (또는 개발자의 추측이 최선의 방법으로 회복하는 것)는 잡히지 않는 길입니다.
보리스 칼 렌스

2
+1 이것은 좋은 대답입니다. 값 유형의 경우 기본값을 고려해야하기 때문입니다. 이를 통해 동작을 통해 기본값을 "설정"할 수 있습니다.
IllidanS4는 Monica가

이것이 정확히 Nullable<T>(예 :)와 같은 클래스를 구현하는 방법 int?입니다.
Jonathan Allen

아주 나쁜 생각입니다. 0/0은 항상 유효하지 않은 분수 (NaN) 여야합니다. 누군가 new Rational(x,y)x와 y가 0 인 곳을 호출하면 어떻게 될까요?
Mike Rosoft

실제 생성자가있는 경우 예외가 발생하여 실제 0/0이 발생하지 않도록 할 수 있습니다. 또는 원하는 경우 기본과 0/0을 구분하기 위해 추가 부울을 추가해야합니다.
조나단 앨런

2

내가 사용하는 것은 다음 과 같은 백업 필드와 결합 된 null 병합 연산자 (??)입니다 .

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

  public SomeRefType MyRefVariable {
    get { return m_MyRefVariableBackingField ?? (m_MyRefVariableBackingField = new SomeRefType()); }
  }
}

도움이 되었기를 바랍니다 ;)

참고 : null 병합 할당 은 현재 C # 8.0의 기능 제안입니다.


1

C #을 사용하고 있으므로 기본 생성자를 정의 할 수 없습니다.

Structs는 .NET에서 기본 생성자를 가질 수 있지만 지원하는 특정 언어는 알지 못합니다.


C #에서 클래스와 구조체는 의미 적으로 다릅니다. 구조체는 값 형식이고 클래스는 참조 형식입니다.
Tom Sarduy

-1

기본 생성자 딜레마가없는 솔루션이 있습니다. 나는 이것이 늦은 해결책이라는 것을 알고 있지만 이것이 해결책이라는 점은 주목할 가치가 있다고 생각합니다.

public struct Point2D {
    public static Point2D NULL = new Point2D(-1,-1);
    private int[] Data;

    public int X {
        get {
            return this.Data[ 0 ];
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 0 ] = value;
            }
        }
    }

    public int Z {
        get {
            return this.Data[ 1 ];
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 1 ] = value;
            }
        }
    }

    public Point2D( int x , int z ) {
        this.Data = new int[ 2 ] { x , z };
    }

    public static Point2D operator +( Point2D A , Point2D B ) {
        return new Point2D( A.X + B.X , A.Z + B.Z );
    }

    public static Point2D operator -( Point2D A , Point2D B ) {
        return new Point2D( A.X - B.X , A.Z - B.Z );
    }

    public static Point2D operator *( Point2D A , int B ) {
        return new Point2D( B * A.X , B * A.Z );
    }

    public static Point2D operator *( int A , Point2D B ) {
        return new Point2D( A * B.Z , A * B.Z );
    }

    public override string ToString() {
        return string.Format( "({0},{1})" , this.X , this.Z );
    }
}

get; set;을 사용하여 null이라는 정적 구조체가 있다는 사실을 무시하고 (참고 : 이것은 모든 양의 사분면에만 해당) get; set; C #에서는 특정 데이터 형식이 기본 생성자 Point2D ()에 의해 초기화되지 않은 오류를 처리하기 위해 try / catch / finally를 가질 수 있습니다. 나는 이것이이 답변에 대한 일부 사람들에게 해결책으로 애매하다고 생각합니다. 그게 주로 내가 추가하는 이유입니다. C #에서 getter 및 setter 기능을 사용하면이 기본 생성자 인 non-sense를 무시하고 초기화하지 않은 항목을 시도해 볼 수 있습니다. 나를 위해 이것은 잘 작동합니다. 다른 사람을 위해 if 문을 추가하고 싶을 수도 있습니다. 따라서 Numerator / Denominator 설정을 원할 경우이 코드가 도움이 될 수 있습니다. 이 솔루션이 멋지게 보이지 않고 효율성 측면에서 더 나쁘게 작동한다고 반복하고 싶습니다. 이전 버전의 C #에서 온 누군가를 위해 배열 데이터 형식을 사용하면이 기능이 제공됩니다. 작동하는 것을 원한다면 다음을 시도하십시오.

public struct Rational {
    private long[] Data;

    public long Numerator {
        get {
            try {
                return this.Data[ 0 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 0 ];
            }
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 0 ] = value;
            }
        }
    }

    public long Denominator {
        get {
            try {
                return this.Data[ 1 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 1 ];
            }
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 1 ] = value;
            }
        }
    }

    public Rational( long num , long denom ) {
        this.Data = new long[ 2 ] { num , denom };
        /* Todo: Find GCD etc. */
    }

    public Rational( long num ) {
        this.Data = new long[ 2 ] { num , 1 };
        this.Numerator = num;
        this.Denominator = 1;
    }
}

2
이것은 매우 나쁜 코드입니다. 왜 구조체에 배열 참조가 있습니까? 왜 X와 Y 좌표를 필드로 사용하지 않습니까? 흐름 제어에 예외를 사용하는 것은 좋지 않습니다. 일반적으로 NullReferenceException이 발생하지 않는 방식으로 코드를 작성해야합니다. 실제로 이것이 필요한 경우-그러한 구조가 구조체가 아닌 클래스에 더 적합하지만 지연 초기화를 사용해야합니다. (그리고 기술적으로, 당신은 좌표의 첫 번째 설정을 제외하고는 완전히 불필요하게 각 좌표를 두 번 설정합니다.)
Mike Rosoft

-1
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}

5
허용되지만 매개 변수를 지정하지 않으면 사용되지 않습니다. ideone.com/xsLloQ
Motti
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.