다음을 통해 공유


C# 및 .NET의 상속

이 자습서에서는 C#에서 상속을 소개합니다. 상속은 특정 기능(데이터 및 동작)을 제공하는 기본 클래스를 정의하고 해당 기능을 상속하거나 재정의하는 파생 클래스를 정의할 수 있는 개체 지향 프로그래밍 언어의 기능입니다.

필수 조건

설치 지침

Windows에서 이 WinGet 구성 파일을 사용하여 모든 필수 구성 요소를 설치합니다. 이미 설치된 항목이 있는 경우 WinGet은 해당 단계를 건너뜁니다.

  1. 파일을 다운로드하고 두 번 클릭하여 실행합니다.
  2. 사용권 계약을 읽고, y입력하고, 동의하라는 메시지가 표시되면 Enter 키를 선택합니다.
  3. 작업 표시줄에 깜박이는 UAC(사용자 계정 컨트롤) 프롬프트가 표시되면 설치를 계속하도록 허용합니다.

다른 플랫폼에서는 이러한 각 구성 요소를 별도로 설치해야 합니다.

  1. .NET SDK 다운로드 페이지권장 설치 관리자를 다운로드하고 두 번 클릭하여 실행합니다. 다운로드 페이지에서 플랫폼을 검색하고 플랫폼에 대한 최신 설치 관리자를 권장합니다.
  2. Visual Studio Code 홈페이지에서 최신 설치 관리자를 다운로드하고 두 번 클릭하여 실행합니다. 그 페이지는 또한 플랫폼을 탐지하며, 링크는 시스템에 맞게 정확할 것입니다.
  3. C# DevKit 확장 페이지에서 "설치" 단추를 클릭합니다. 그러면 Visual Studio 코드가 열리고 확장을 설치하거나 사용하도록 설정할지 묻습니다. "설치"를 선택합니다.

예제 실행

이 자습서의 예제를 만들고 실행하려면 명령줄에서 dotnet 유틸리티를 사용합니다. 각 예제에 대해 다음 단계를 수행합니다.

  1. 예제를 저장할 디렉터리를 만듭니다.

  2. 명령 프롬프트에서 dotnet 새 콘솔 명령을 입력하여 새 .NET Core 프로젝트를 만듭니다.

  3. 예제의 코드를 복사하여 코드 편집기에 붙여넣습니다.

  4. 명령줄에서 dotnet restore 명령을 입력하여 프로젝트의 종속성을 로드하거나 복원합니다.

    dotnet restore, dotnet new, dotnet build, dotnet run, dotnet testdotnet publish같은 복원이 필요한 모든 명령에서 암시적으로 실행되므로 dotnet pack 실행할 필요가 없습니다. 암시적 복원을 사용하지 않도록 설정하려면 --no-restore 옵션을 사용합니다.

    dotnet restore 명령은 Azure DevOps Services의 연속 통합 빌드 또는 복원이 언제 발생할지를 명시적으로 제어해야 하는 빌드 시스템처럼 명시적인 복원이 적합한 특정 시나리오에서 여전히 유용합니다.

    NuGet 피드를 관리하는 방법에 대한 자세한 내용은 dotnet restore 설명서참조하세요.

  5. dotnet run 명령을 입력하여 예제를 컴파일하고 실행합니다.

배경: 상속이란?

상속 개체 지향 프로그래밍의 기본 특성 중 하나입니다. 이를 통해 부모 클래스의 동작을 재사용(상속), 확장 또는 수정하는 자식 클래스를 정의할 수 있습니다. 멤버가 상속되는 클래스를 기본 클래스라고 합니다. 기본 클래스의 멤버를 상속하는 클래스를 파생 클래스라고 합니다.

C# 및 .NET은 단일 상속 지원합니다. 즉, 클래스는 단일 클래스에서만 상속할 수 있습니다. 그러나 상속은 전이적이므로 형식 집합에 대한 상속 계층 구조를 정의할 수 있습니다. 즉, D 형식은 C형식에서 상속받고, C형식은 A형식에서 상속받으며, A형식은 기본 클래스 형식에서 상속받습니다. 상속은 전이적이므로 A 형식의 멤버를 D형식에 사용할 수 있습니다.

기본 클래스의 모든 멤버가 파생 클래스에서 상속되는 것은 아닙니다. 다음 멤버는 상속되지 않습니다.

기본 클래스의 다른 모든 멤버는 파생 클래스에 의해 상속되지만 표시되는지 여부는 접근성에 따라 달라집니다. 멤버의 접근성은 다음과 같이 파생 클래스의 표시 유형에 영향을 줍니다.

  • Private 멤버는 기본 클래스에 중첩된 파생 클래스에서만 표시됩니다. 그렇지 않으면 파생 클래스에 표시되지 않습니다. 다음 예제에서 A.BA파생되고 CA파생되는 중첩 클래스입니다. 프라이빗 A._value 필드는 A.B.에 표시됩니다. 그러나 C.GetValue 메서드에서 주석을 제거하고 예제를 컴파일하려고 하면 컴파일러 오류 CS0122가 생성됩니다. "'A._value'은 보호 수준으로 인해 액세스할 수 없습니다."

    public class A
    {
        private int _value = 10;
    
        public class B : A
        {
            public int GetValue()
            {
                return _value;
            }
        }
    }
    
    public class C : A
    {
        //    public int GetValue()
        //    {
        //        return _value;
        //    }
    }
    
    public class AccessExample
    {
        public static void Main(string[] args)
        {
            var b = new A.B();
            Console.WriteLine(b.GetValue());
        }
    }
    // The example displays the following output:
    //       10
    
  • 보호된 멤버는 파생 클래스에서만 볼 수 있습니다.

  • 내부 멤버는 기본 클래스와 동일한 어셈블리에 있는 파생 클래스에서만 볼 수 있습니다. 기본 클래스와 다른 어셈블리에 있는 파생 클래스에는 표시되지 않습니다.

  • Public 멤버는 파생 클래스에 표시되며 파생 클래스의 공용 인터페이스에 속합니다. 퍼블릭 상속된 멤버는 파생 클래스에 정의된 것처럼 호출할 수 있습니다. 다음 예제에서 클래스 AMethod1메서드를 정의하고 클래스 B 클래스 A상속합니다. 그런 다음 Method1인스턴스 메서드인 것처럼 B 호출합니다.

    public class A
    {
        public void Method1()
        {
            // Method implementation.
        }
    }
    
    public class B : A
    { }
    
    public class Example
    {
        public static void Main()
        {
            B b = new ();
            b.Method1();
        }
    }
    

파생 클래스는 대체 구현을 제공하여 상속된 멤버를 재정의할 수도 있습니다. 멤버를 재정의하려면 기본 클래스의 멤버를 가상 키워드로 표시해야 합니다. 기본적으로 기본 클래스 멤버는 virtual 표시되지 않으며 재정의할 수 없습니다. 다음 예제와 같이 가상이 아닌 멤버를 재정의하려고 하면 컴파일러 오류 CS0506이 생성됩니다. "<멤버> 가상, 추상 또는 재정의로 표시되지 않으므로 상속된 멤버 <멤버> 재정의할 수 없습니다."

public class A
{
    public void Method1()
    {
        // Do something.
    }
}

public class B : A
{
    public override void Method1() // Generates CS0506.
    {
        // Do something else.
    }
}

때때로 파생 클래스 은(는) 기본 클래스 구현을 재정의해야 합니다. 추상 키워드로 표시된 기본 클래스 멤버는 파생 클래스에서 오버라이드해야 합니다. 다음 예제를 컴파일하려고 하면 컴파일러 오류 CS0534가 생성됩니다. "<클래스> 상속된 추상 멤버 <멤버>"를 구현하지 않습니다. 클래스 BA.Method1대한 구현을 제공하지 않기 때문입니다.

public abstract class A
{
    public abstract void Method1();
}

public class B : A // Generates CS0534.
{
    public void Method3()
    {
        // Do something.
    }
}

상속은 클래스 및 인터페이스에만 적용됩니다. 다른 데이터 형식 또는 타입 카테고리(구조체, 대리자 및 열거형)는 상속을 제공하지 않습니다. 이러한 규칙 때문에 다음 예제와 같은 코드를 컴파일하려고 시도하면 컴파일러 오류 CS0527이 생성됩니다. "인터페이스 목록에서 'ValueType' 형식은 인터페이스가 아닙니다." 오류 메시지는 구조체가 구현하는 인터페이스를 정의할 수 있지만 상속은 지원되지 않음을 나타냅니다.

public struct ValueStructure : ValueType // Generates CS0527.
{
}

암시적 상속

단일 상속을 통해 상속할 수 있는 모든 형식 외에도 .NET 형식 시스템의 모든 형식은 암시적으로 Object 또는 파생된 형식에서 상속됩니다. Object의 공통 기능은 모든 형식에서 사용할 수 있습니다.

암시적 상속의 의미를 확인하려면 단순히 빈 클래스 정의인 새 클래스 SimpleClass정의해 보겠습니다.

public class SimpleClass
{ }

그런 다음 리플렉션(형식의 메타데이터를 검사하여 해당 형식에 대한 정보를 가져올 수 있도록 허용)을 사용하여 SimpleClass 형식에 속한 멤버 목록을 가져올 수 있습니다. SimpleClass 클래스에서 멤버를 정의하지는 않았지만 예제의 출력은 실제로 9개의 멤버가 있음을 나타냅니다. 이러한 멤버 중 하나는 C# 컴파일러에서 SimpleClass 형식에 대해 자동으로 제공되는 매개 변수가 없는(또는 기본) 생성자입니다. 나머지 8개는 .NET 형식 시스템의 모든 클래스와 인터페이스가 궁극적으로 암시적으로 상속되는 형식인 Object멤버입니다.

using System.Reflection;

public class SimpleClassExample
{
    public static void Main()
    {
        Type t = typeof(SimpleClass);
        BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
                             BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
        MemberInfo[] members = t.GetMembers(flags);
        Console.WriteLine($"Type {t.Name} has {members.Length} members: ");
        foreach (MemberInfo member in members)
        {
            string access = "";
            string stat = "";
            var method = member as MethodBase;
            if (method != null)
            {
                if (method.IsPublic)
                    access = " Public";
                else if (method.IsPrivate)
                    access = " Private";
                else if (method.IsFamily)
                    access = " Protected";
                else if (method.IsAssembly)
                    access = " Internal";
                else if (method.IsFamilyOrAssembly)
                    access = " Protected Internal ";
                if (method.IsStatic)
                    stat = " Static";
            }
            string output = $"{member.Name} ({member.MemberType}): {access}{stat}, Declared by {member.DeclaringType}";
            Console.WriteLine(output);
        }
    }
}
// The example displays the following output:
//	Type SimpleClass has 9 members:
//	ToString (Method):  Public, Declared by System.Object
//	Equals (Method):  Public, Declared by System.Object
//	Equals (Method):  Public Static, Declared by System.Object
//	ReferenceEquals (Method):  Public Static, Declared by System.Object
//	GetHashCode (Method):  Public, Declared by System.Object
//	GetType (Method):  Public, Declared by System.Object
//	Finalize (Method):  Internal, Declared by System.Object
//	MemberwiseClone (Method):  Internal, Declared by System.Object
//	.ctor (Constructor):  Public, Declared by SimpleClass

Object 클래스에서 암시적 상속을 사용하면 이러한 메서드를 SimpleClass 클래스에서 사용할 수 있습니다.

  • ToString 개체를 문자열 표현으로 변환하는 public SimpleClass 메서드는 정규화된 형식 이름을 반환합니다. 이 경우 ToString 메서드는 "SimpleClass" 문자열을 반환합니다.

  • 두 개체의 같음을 테스트하는 세 가지 메서드는 public 인스턴스 Equals(Object) 메서드, public static Equals(Object, Object) 메서드 및 public static ReferenceEquals(Object, Object) 메서드입니다. 기본적으로 이러한 메서드는 참조 같음을 테스트합니다. 즉, 같게 하려면 두 개의 개체 변수가 동일한 개체를 참조해야 합니다.

  • 해시된 컬렉션에서 형식의 인스턴스를 사용할 수 있는 값을 계산하는 public GetHashCode 메서드입니다.

  • GetType 형식을 나타내는 Type 개체를 반환하는 public SimpleClass 메서드입니다.

  • 가비지 수집기에서 개체의 메모리를 회수하기 전에 관리되지 않는 리소스를 해제하도록 설계된 보호된 Finalize 메서드입니다.

  • 현재 개체의 단순 복제본을 만드는 보호된 MemberwiseClone 메서드입니다.

암시적 상속으로 인해 실제로 SimpleClass 클래스에 정의된 멤버인 것처럼 SimpleClass 개체에서 상속된 멤버를 호출할 수 있습니다. 예를 들어 다음 예제에서는 SimpleClass.ToString로부터 상속된 SimpleClass 메서드를 Object에서 호출합니다.

public class EmptyClass
{ }

public class ClassNameExample
{
    public static void Main()
    {
        EmptyClass sc = new();
        Console.WriteLine(sc.ToString());
    }
}
// The example displays the following output:
//        EmptyClass

다음 표에서는 C#에서 만들 수 있는 형식의 범주와 암시적으로 상속되는 형식을 나열합니다. 각 기본 형식은 상속을 통해 암시적으로 파생된 형식에 사용할 수 있는 다른 멤버 집합을 만듭니다.

유형 범주 암시적으로 ~로부터 상속받습니다.
수업 Object
구조체 (struct) ValueType, Object
enum Enum, ValueType, Object
사절 MulticastDelegate, Delegate, Object

상속 및 'A는 B이다' 관계

일반적으로 상속은 기본 클래스와 하나 이상의 파생 클래스 간에 "is a" 관계를 표현하는 데 사용됩니다. 여기서 파생 클래스는 기본 클래스의 특수 버전입니다. 파생 클래스는 기본 클래스의 형식입니다. 예를 들어 Publication 클래스는 모든 종류의 게시를 나타내고 BookMagazine 클래스는 특정 유형의 게시를 나타냅니다.

비고

클래스 또는 구조체는 하나 이상의 인터페이스를 구현할 수 있습니다. 인터페이스 구현은 종종 단일 상속에 대한 해결 방법으로 제공되거나 구조체와 상속을 사용하는 방법으로 제공되지만, 인터페이스와 상속이 아닌 구현 형식 간에 다른 관계("할 수 있는" 관계)를 표현하기 위한 것입니다. 인터페이스는 인터페이스가 구현 형식에 사용할 수 있도록 하는 기능의 하위 집합(예: 같음을 테스트하거나, 개체를 비교하거나 정렬하거나, 문화권 구분 구문 분석 및 서식 지정을 지원하는 기능)을 정의합니다.

또한 "is a"는 형식과 해당 형식의 특정 인스턴스화 간의 관계를 표현합니다. 다음 예제에서 Automobile는 세 가지 고유한 읽기 전용 속성을 가진 클래스입니다: Make, 자동차의 제조업체; Model, 자동차의 종류; 및 Year, 제조 연도. 또한 Automobile 클래스에는 인수가 속성 값에 할당된 생성자가 있으며 Object.ToString 메서드를 재정의하여 Automobile 클래스가 아닌 Automobile 인스턴스를 고유하게 식별하는 문자열을 생성합니다.

public class Automobile
{
    public Automobile(string make, string model, int year)
    {
        if (make == null)
            throw new ArgumentNullException(nameof(make), "The make cannot be null.");
        else if (string.IsNullOrWhiteSpace(make))
            throw new ArgumentException("make cannot be an empty string or have space characters only.");
        Make = make;

        if (model == null)
            throw new ArgumentNullException(nameof(model), "The model cannot be null.");
        else if (string.IsNullOrWhiteSpace(model))
            throw new ArgumentException("model cannot be an empty string or have space characters only.");
        Model = model;

        if (year < 1857 || year > DateTime.Now.Year + 2)
            throw new ArgumentException("The year is out of range.");
        Year = year;
    }

    public string Make { get; }

    public string Model { get; }

    public int Year { get; }

    public override string ToString() => $"{Year} {Make} {Model}";
}

이 경우 상속에 의존하여 특정 자동차 및 모델을 나타내면 안 됩니다. 예를 들어 Packard Motor Car Company에서 제조한 자동차를 나타내는 Packard 유형을 정의할 필요가 없습니다. 대신 다음 예제와 같이 해당 클래스 생성자에 전달된 적절한 값을 사용하여 Automobile 개체를 만들어 나타낼 수 있습니다.

using System;

public class Example
{
    public static void Main()
    {
        var packard = new Automobile("Packard", "Custom Eight", 1948);
        Console.WriteLine(packard);
    }
}
// The example displays the following output:
//        1948 Packard Custom Eight

상속을 기반으로 하는 is-a 관계는 기본 클래스 및 기본 클래스에 멤버를 추가하거나 기본 클래스에 없는 추가 기능이 필요한 파생 클래스에 가장 적합합니다.

기본 클래스 및 파생 클래스 디자인

기본 클래스 및 파생 클래스를 디자인하는 프로세스를 살펴보겠습니다. 이 섹션에서는 책, 잡지, 신문, 저널, 기사 등 모든 종류의 출판을 나타내는 기본 클래스 Publication정의합니다. 또한 Book파생되는 Publication 클래스를 정의합니다. 예제를 쉽게 확장하여 Magazine, Journal, NewspaperArticle같은 다른 파생 클래스를 정의할 수 있습니다.

기본 게시 클래스

Publication 클래스를 디자인할 때는 다음과 같은 몇 가지 디자인 결정을 내려야 합니다.

  • 기본 Publication 클래스에 포함할 멤버 및 Publication 멤버가 메서드 구현을 제공하는지 또는 Publication 파생 클래스에 대한 템플릿 역할을 하는 추상 기본 클래스인지 여부입니다.

    이 경우 Publication 클래스는 메서드 구현을 제공합니다. 추상 기본 클래스 디자인 및 파생 클래스 섹션에는 추상 기본 클래스를 사용하여 파생 클래스가 재정의해야 하는 메서드를 정의하는 예제가 포함되어 있습니다. 파생 클래스는 파생 형식에 적합한 모든 구현을 자유롭게 제공할 수 있습니다.

    코드를 다시 사용하는 기능(즉, 여러 파생 클래스가 기본 클래스 메서드의 선언 및 구현을 공유하고 재정의할 필요가 없음)은 추상이 아닌 기본 클래스의 장점입니다. 따라서 일부 또는 대부분의 특수한 Publication 형식에서 코드를 공유할 가능성이 있는 경우, Publication에 멤버를 추가해야 합니다. 기본 클래스 구현을 효율적으로 제공하지 못하는 경우 기본 클래스의 단일 구현이 아니라 파생 클래스에서 거의 동일한 멤버 구현을 제공해야 합니다. 여러 위치에서 중복된 코드를 유지 관리해야 하는 것은 버그의 잠재적인 원인입니다.

    코드 재사용을 최대화하고 논리적이고 직관적인 상속 계층 구조를 만들려면 모두 또는 대부분의 게시에 공통적인 데이터와 기능만 Publication 클래스에 포함해야 합니다. 그런 다음 파생 클래스는 자신이 나타내는 특정 종류의 게시에 고유한 멤버를 구현합니다.

  • 클래스 계층 구조를 얼마나 확장할 것인지 결정하는 방법. 단순히 기본 클래스와 하나 이상의 파생 클래스가 아닌 세 개 이상의 클래스 계층 구조를 개발하시겠습니까? 예를 들어, PublicationPeriodical의 기본 클래스일 수 있으며, PeriodicalJournal, Newspaper 및 의 기본 클래스일 수 있습니다.

    예제에서는 Publication 클래스의 작은 계층 구조와 단일 파생 클래스 Book사용합니다. 예제를 쉽게 확장하여 PublicationMagazine같은 Article파생되는 여러 추가 클래스를 만들 수 있습니다.

  • 기본 클래스를 인스턴스화하는 것이 적합한지 여부입니다. 그렇지 않은 경우 추상 키워드를 클래스에 적용해야 합니다. 그렇지 않으면 클래스 생성자를 호출하여 Publication 클래스를 인스턴스화할 수 있습니다. 클래스 생성자를 직접 호출하여 abstract 키워드로 표시된 클래스를 인스턴스화하려고 하면 C# 컴파일러는 "추상 클래스 또는 인터페이스의 인스턴스를 만들 수 없습니다."라는 오류 CS0144를 생성합니다. 리플렉션을 사용하여 클래스를 인스턴스화하려고 하면 리플렉션 메서드는 MemberAccessExceptionthrow합니다.

    기본적으로 기본 클래스는 해당 클래스 생성자를 호출하여 인스턴스화할 수 있습니다. 클래스 생성자를 명시적으로 정의할 필요는 없습니다. 기본 클래스의 소스 코드에 없는 경우 C# 컴파일러는 자동으로 기본(매개 변수 없는) 생성자를 제공합니다.

    예제에서는 인스턴스화할 수 없도록 Publication 클래스를 추상 표시합니다. abstract 메서드가 없는 abstract 클래스는 이 클래스가 여러 구체적인 클래스(예: Book, Journal) 간에 공유되는 추상 개념을 나타낸다는 것을 나타냅니다.

  • 파생 클래스가 특정 멤버의 기본 클래스 구현을 상속해야 하는지 여부, 기본 클래스 구현을 재정의할 수 있는 옵션이 있는지 여부 또는 구현을 제공해야 하는지 여부입니다. 추상 키워드를 사용하여 파생 클래스가 구현을 제공하도록 강제합니다. 가상 키워드를 사용하여 파생 클래스가 기본 클래스 메서드를 재정의할 수 있도록 합니다. 기본적으로 기본 클래스에 정의된 메서드는 재정의할 수 없습니다.

    Publication 클래스에는 abstract 메서드가 없지만 클래스 자체는 abstract.

  • 파생 클래스가 상속 계층 구조의 최종 클래스를 나타내며 추가 파생 클래스의 기본 클래스로 사용할 수 없는지 여부입니다. 기본적으로 모든 클래스는 기본 클래스로 사용할 수 있습니다. 봉인된 키워드를 적용하여 클래스가 추가 클래스의 기본 클래스로 사용될 수 없음을 나타낼 수 있습니다. 봉인된 클래스로부터 파생을 시도하면 컴파일러 오류 CS0509가 생성됩니다. "봉인된 형식 typeName<>에서 파생될 수 없습니다."

    예제에서는 파생 클래스를 sealed표시합니다.

다음 예제에서는 Publication 클래스에 대 한 소스 코드 뿐만 아니라 PublicationType 속성에서 반환 되는 Publication.PublicationType 열거형입니다. Object상속하는 멤버 외에도 Publication 클래스는 다음과 같은 고유한 멤버 및 멤버 재정의를 정의합니다.


public enum PublicationType { Misc, Book, Magazine, Article };

public abstract class Publication
{
    private bool _published = false;
    private DateTime _datePublished;
    private int _totalPages;

    public Publication(string title, string publisher, PublicationType type)
    {
        if (string.IsNullOrWhiteSpace(publisher))
            throw new ArgumentException("The publisher is required.");
        Publisher = publisher;

        if (string.IsNullOrWhiteSpace(title))
            throw new ArgumentException("The title is required.");
        Title = title;

        Type = type;
    }

    public string Publisher { get; }

    public string Title { get; }

    public PublicationType Type { get; }

    public string? CopyrightName { get; private set; }

    public int CopyrightDate { get; private set; }

    public int Pages
    {
        get { return _totalPages; }
        set
        {
            if (value <= 0)
                throw new ArgumentOutOfRangeException(nameof(value), "The number of pages cannot be zero or negative.");
            _totalPages = value;
        }
    }

    public string GetPublicationDate()
    {
        if (!_published)
            return "NYP";
        else
            return _datePublished.ToString("d");
    }

    public void Publish(DateTime datePublished)
    {
        _published = true;
        _datePublished = datePublished;
    }

    public void Copyright(string copyrightName, int copyrightDate)
    {
        if (string.IsNullOrWhiteSpace(copyrightName))
            throw new ArgumentException("The name of the copyright holder is required.");
        CopyrightName = copyrightName;

        int currentYear = DateTime.Now.Year;
        if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2)
            throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
        CopyrightDate = copyrightDate;
    }

    public override string ToString() => Title;
}
  • 생성자

    Publication 클래스는 abstract이기 때문에, 다음 예제와 같이 코드에서 직접 인스턴스화할 수 없습니다.

    var publication = new Publication("Tiddlywinks for Experts", "Fun and Games",
                                      PublicationType.Book);
    

    그러나 해당 인스턴스 생성자는 Book 클래스의 소스 코드와 같이 파생 클래스 생성자에서 직접 호출할 수 있습니다.

  • 게시 관련 속성 2개

    Title String 생성자를 호출하여 값을 제공하는 읽기 전용 Publication 속성입니다.

    Pages 발행물의 총 페이지 수를 나타내는 읽기-쓰기 Int32 속성입니다. 값은 totalPages프라이빗 필드에 저장됩니다. 양수여야 하며, 그렇지 않으면 ArgumentOutOfRangeException가 던져집니다.

  • 게시자 관련 멤버

    PublisherType두 개의 읽기 전용 속성입니다. 값은 원래 Publication 클래스 생성자에 대한 호출에 의해 제공됩니다.

  • 출판 관련 회원

    PublishGetPublicationDate두 메서드는 게시 날짜를 설정하고 반환합니다. Publish 메서드는 프라이빗 published 플래그가 호출될 때 true 설정하고 전달된 날짜를 프라이빗 datePublished 필드에 인수로 할당합니다. GetPublicationDate 메서드는 published 플래그가 false경우 문자열 "NYP"를 반환하고 datePublished경우 true 필드의 값을 반환합니다.

  • 저작권 관련 구성원

    Copyright 방법은 저작권 소유자의 이름과 저작권의 연도를 인수로 사용하여 CopyrightNameCopyrightDate 속성에 할당합니다.

  • ToString 메서드의 재정의

    형식이 Object.ToString 메서드를 재정의하지 않으면 형식의 정규화된 이름을 반환합니다. 이 이름은 한 인스턴스를 다른 인스턴스와 구분하는 데 거의 사용되지 않습니다. Publication 클래스는 Object.ToString 속성의 값을 반환하기 위해 Title을(를) 재정의합니다.

다음 그림에서는 기본 Publication 클래스와 암시적으로 상속된 Object 클래스 간의 관계를 보여 줍니다.

개체 및 게시 클래스The Object and Publication classesThe Object and Publication classes

Book 클래스

Book 클래스는 책을 특수 발행물 형식으로 나타냅니다. 다음 예제에서는 Book 클래스의 소스 코드를 보여 있습니다.

using System;

public sealed class Book : Publication
{
    public Book(string title, string author, string publisher) :
           this(title, string.Empty, author, publisher)
    { }

    public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
    {
        // isbn argument must be a 10- or 13-character numeric string without "-" characters.
        // We could also determine whether the ISBN is valid by comparing its checksum digit
        // with a computed checksum.
        //
        if (!string.IsNullOrEmpty(isbn))
        {
            // Determine if ISBN length is correct.
            if (!(isbn.Length == 10 | isbn.Length == 13))
                throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
            if (!ulong.TryParse(isbn, out _))
                throw new ArgumentException("The ISBN can consist of numeric characters only.");
        }
        ISBN = isbn;

        Author = author;
    }

    public string ISBN { get; }

    public string Author { get; }

    public decimal Price { get; private set; }

    // A three-digit ISO currency symbol.
    public string? Currency { get; private set; }

    // Returns the old price, and sets a new price.
    public decimal SetPrice(decimal price, string currency)
    {
        if (price < 0)
            throw new ArgumentOutOfRangeException(nameof(price), "The price cannot be negative.");
        decimal oldValue = Price;
        Price = price;

        if (currency.Length != 3)
            throw new ArgumentException("The ISO currency symbol is a 3-character string.");
        Currency = currency;

        return oldValue;
    }

    public override bool Equals(object? obj)
    {
        if (obj is not Book book)
            return false;
        else
            return ISBN == book.ISBN;
    }

    public override int GetHashCode() => ISBN.GetHashCode();

    public override string ToString() => $"{(string.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}

Publication상속하는 멤버 외에도 Book 클래스는 다음과 같은 고유한 멤버 및 멤버 재정의를 정의합니다.

  • 두 개의 생성자

    Book 생성자는 세 가지 공통 매개 변수를 공유합니다. 두 개의 제목게시자Publication 생성자의 매개 변수에 해당합니다. 세 번째는 저자이며, 이는 공용 불변 Author 속성에 저장됩니다. 하나의 생성자에는 자동 속성에 저장되는 ISBN 매개 변수가 포함됩니다.

    첫 번째 생성자는 이 키워드를 사용하여 다른 생성자를 호출합니다. 생성자 체인은 생성자를 정의하는 일반적인 패턴입니다. 매개 변수 수가 적은 생성자는 가장 많은 수의 매개 변수를 사용하여 생성자를 호출할 때 기본값을 제공합니다.

    두 번째 생성자는 기본 키워드를 사용하여 타이틀 및 게시자 이름을 기본 클래스 생성자에 전달합니다. 소스 코드에서 기본 클래스 생성자를 명시적으로 호출하지 않으면 C# 컴파일러가 기본 클래스의 기본 또는 매개 변수 없는 생성자에 대한 호출을 자동으로 제공합니다.

  • 읽기 전용 ISBN 속성으로, Book 개체의 국제 표준 도서 번호(고유한 10자리 또는 13자리 숫자)를 반환합니다. ISBN은 Book 생성자 중 하나에 인수로 제공됩니다. ISBN은 컴파일러에서 자동으로 생성되는 프라이빗 지원 필드에 저장됩니다.

  • 읽기 전용 Author 속성입니다. 작성자 이름은 Book 생성자 모두에 대한 인수로 제공되며 속성에 저장됩니다.

  • PriceCurrency두 개의 읽기 전용 가격 관련 속성입니다. 해당 값은 SetPrice 메서드 호출에서 인수로 제공됩니다. Currency 속성은 3자리 ISO 통화 기호(예: 미국 달러)입니다. ISO 통화 기호는 ISOCurrencySymbol 속성에서 검색할 수 있습니다. 이러한 두 속성은 모두 외부에서 읽기 전용이지만 둘 다 Book 클래스의 코드로 설정할 수 있습니다.

  • SetPricePrice 속성의 값을 설정하는 Currency 메서드입니다. 이러한 값은 동일한 속성에서 반환됩니다.

  • ToString에서 상속된 Publication 메서드와 Object.Equals(Object)에서 상속된 GetHashCodeObject 메서드를 재정의합니다.

    재정의되지 않는 한 Object.Equals(Object) 메서드는 참조 동일성을 테스트합니다. 즉, 동일한 개체를 참조하는 경우 두 개체 변수가 같은 것으로 간주됩니다. 반면 Book 클래스에서는 ISBN이 같으면 두 개의 Book 개체가 같아야 합니다.

    Object.Equals(Object) 메서드를 재정의하는 경우 런타임이 효율적인 검색을 위해 해시된 컬렉션에 항목을 저장하는 데 사용하는 값을 반환하는 GetHashCode 메서드도 재정의해야 합니다. 해시 코드는 같음 테스트와 일치하는 값을 반환해야 합니다. 두 Object.Equals(Object) 개체의 ISBN 속성이 같으면 true 반환하도록 Book 재정의했으므로 GetHashCode 속성에서 반환된 문자열의 ISBN 메서드를 호출하여 계산된 해시 코드를 반환합니다.

다음 그림에서는 Book 클래스와 기본 클래스인 Publication간의 관계를 보여 줍니다.

출판물 및 책 클래스

이제 다음 예제와 같이 Book 개체를 인스턴스화하고 고유 멤버와 상속된 멤버를 모두 호출하고 Publication 형식 또는 Book형식의 매개 변수를 예상하는 메서드에 인수로 전달할 수 있습니다.

public class ClassExample
{
    public static void Main()
    {
        var book = new Book("The Tempest", "0971655819", "Shakespeare, William",
                            "Public Domain Press");
        ShowPublicationInfo(book);
        book.Publish(new DateTime(2016, 8, 18));
        ShowPublicationInfo(book);

        var book2 = new Book("The Tempest", "Classic Works Press", "Shakespeare, William");
        Console.Write($"{book.Title} and {book2.Title} are the same publication: " +
              $"{((Publication)book).Equals(book2)}");
    }

    public static void ShowPublicationInfo(Publication pub)
    {
        string pubDate = pub.GetPublicationDate();
        Console.WriteLine($"{pub.Title}, " +
                  $"{(pubDate == "NYP" ? "Not Yet Published" : "published on " + pubDate):d} by {pub.Publisher}");
    }
}
// The example displays the following output:
//        The Tempest, Not Yet Published by Public Domain Press
//        The Tempest, published on 8/18/2016 by Public Domain Press
//        The Tempest and The Tempest are the same publication: False

추상 기본 클래스 및 파생 클래스 디자인

이전 예제에서는 파생 클래스가 코드를 공유할 수 있도록 여러 메서드에 대한 구현을 제공하는 기본 클래스를 정의했습니다. 그러나 대부분의 경우 기본 클래스는 구현을 제공하지 않을 것으로 예상됩니다. 대신 기본 클래스는 추상 메서드를 선언하는 추상 클래스; 각 파생 클래스에서 구현해야 하는 멤버를 정의하는 템플릿으로 사용됩니다. 일반적으로 추상 기본 클래스에서 각 파생 형식의 구현은 해당 형식에 고유합니다. 클래스가 출판물에 공통된 기능의 구현을 제공했음에도 불구하고, Publication 객체를 인스턴스화하는 것은 의미가 없기 때문에, 클래스를 추상 키워드로 지정했습니다.

예를 들어 닫힌 각 2차원 기하학적 도형에는 두 가지 속성인 영역, 셰이프의 내부 범위, 및 경계 또는 셰이프의 가장자리를 따라의 거리입니다. 그러나 이러한 속성을 계산하는 방법은 특정 셰이프에 완전히 따라 달라집니다. 예를 들어 원의 경계(또는 둘레)를 계산하는 수식은 사각형의 수식과 다릅니다. Shape 클래스는 abstract 메서드를 사용하는 abstract 클래스입니다. 파생 클래스는 동일한 기능을 공유하지만 파생 클래스는 해당 기능을 다르게 구현한다는 것을 나타냅니다.

다음 예제에서는 ShapeArea두 가지 속성을 정의하는 Perimeter이라는 추상 기본 클래스를 정의합니다. 클래스를 추상 키워드로 표시하는 것 외에도 각 인스턴스 멤버는 추상 키워드로 표시됩니다. 이 경우, Shape은(는) 정규화된 전체 이름 대신 형식의 이름을 반환하도록 Object.ToString 메서드를 재정의합니다. 또한 호출자가 파생 클래스 인스턴스의 영역과 경계를 쉽게 검색할 수 있도록 GetAreaGetPerimeter두 개의 정적 멤버를 정의합니다. 파생 클래스의 인스턴스를 이러한 메서드 중 하나에 전달하는 경우 런타임은 파생 클래스의 메서드 재정의를 호출합니다.

public abstract class Shape
{
    public abstract double Area { get; }

    public abstract double Perimeter { get; }

    public override string ToString() => GetType().Name;

    public static double GetArea(Shape shape) => shape.Area;

    public static double GetPerimeter(Shape shape) => shape.Perimeter;
}

그런 다음 특정 셰이프를 나타내는 Shape 일부 클래스를 파생시킬 수 있습니다. 다음 예제에서는 Square, RectangleCircle세 가지 클래스를 정의합니다. 각각은 특정 셰이프에 고유한 수식을 사용하여 영역 및 경계를 계산합니다. 파생 클래스 중 일부는 나타내는 셰이프에 고유한 Rectangle.DiagonalCircle.Diameter같은 속성을 정의합니다.

using System;

public class Square : Shape
{
    public Square(double length)
    {
        Side = length;
    }

    public double Side { get; }

    public override double Area => Math.Pow(Side, 2);

    public override double Perimeter => Side * 4;

    public double Diagonal => Math.Round(Math.Sqrt(2) * Side, 2);
}

public class Rectangle : Shape
{
    public Rectangle(double length, double width)
    {
        Length = length;
        Width = width;
    }

    public double Length { get; }

    public double Width { get; }

    public override double Area => Length * Width;

    public override double Perimeter => 2 * Length + 2 * Width;

    public bool IsSquare() => Length == Width;

    public double Diagonal => Math.Round(Math.Sqrt(Math.Pow(Length, 2) + Math.Pow(Width, 2)), 2);
}

public class Circle : Shape
{
    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double Area => Math.Round(Math.PI * Math.Pow(Radius, 2), 2);

    public override double Perimeter => Math.Round(Math.PI * 2 * Radius, 2);

    // Define a circumference, since it's the more familiar term.
    public double Circumference => Perimeter;

    public double Radius { get; }

    public double Diameter => Radius * 2;
}

다음 예제에서는 Shape파생된 개체를 사용합니다. Shape 파생된 개체 배열을 인스턴스화하고 반환 Shape 속성 값을 래핑하는 Shape 클래스의 정적 메서드를 호출합니다. 런타임은 파생 형식의 재정의된 속성에서 값을 검색합니다. 또한 이 예제에서는 배열의 각 Shape 개체를 파생 형식으로 캐스팅하고, 캐스트가 성공하면 해당 특정 하위 클래스의 속성을 Shape검색합니다.

using System;

public class Example
{
    public static void Main()
    {
        Shape[] shapes = { new Rectangle(10, 12), new Square(5),
                    new Circle(3) };
        foreach (Shape shape in shapes)
        {
            Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
                              $"perimeter, {Shape.GetPerimeter(shape)}");
            if (shape is Rectangle rect)
            {
                Console.WriteLine($"   Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
                continue;
            }
            if (shape is Square sq)
            {
                Console.WriteLine($"   Diagonal: {sq.Diagonal}");
                continue;
            }
        }
    }
}
// The example displays the following output:
//         Rectangle: area, 120; perimeter, 44
//            Is Square: False, Diagonal: 15.62
//         Square: area, 25; perimeter, 20
//            Diagonal: 7.07
//         Circle: area, 28.27; perimeter, 18.85