프로퍼티의 마지막 시간입니다.

무명 형식, 인터페이스와 프로퍼티, 추상 클래스의 프로퍼티 배울 것이 많아 보입니다.

빠르게 공부해봅시다!

 

# 무명 형식 (Anonymous Type)

제목 그대로 이름이 없는 형식을 무명 형식이라고 합니다.

보통은 임시로 사용하는 경우 특별히 다시 사용하지 않기 때문에 람다처럼 이름을 붙이지 않습니다.

 

무명 형식은 형식의 선언과 동시에 인스턴스를 할당합니다.

인스턴스를 만들고 다시는 사용하지 않을 때 무명 형식이 요긴합니다.

var tempInstance = new {Name = "나, 무명", Age = 20};

무명 형식의 프로퍼티에 할당된 값은 변경 불가능합니다. 읽기만 가능하죠.

나중에 LINQ(링큐)에서 요긴하다고 합니다.

using System;

namespace AnonymousType
{
    class Program
    {
        static void Main()
        {
            // 무명 형식은 형식의 선언과 동시에 인스턴스를 할당, 이후에는 Read만 가능합니다.
            var a = new { Name = "지코", Age = 29 };
            Console.WriteLine($"Name:{a.Name}, Age:{a.Age}");

            var b = new { Subject = "Math", Scores = new int[] { 90, 80, 70, 60 } };
            Console.Write($"Subject:{b.Subject}, Scores: ");
            foreach(var score in b.Scores)
                Console.Write($"{score} ");

            Console.WriteLine();
        }
    }
}
Name:지코, Age:29
Subject:Math, Scores: 90 80 70 60

# 인터페이스의 프로퍼티

기존에도 공부했지만 인터페이스는 메서드, 프로퍼티, 인덱서를 가질 수 있습니다.

게다가 인터페이스에서는 구현부를 만들지 않습니다. 이 개념을 가지고 살펴봅시다.

 

인터페이스에도 프로퍼티를 선언할 수 있는데 구현을 갖지 않습니다. 그래서 프로퍼티는 구현을 갖지 않습니다.

그런데 인터페이스를 상속하는 클래스는 반드시 해당 프로퍼티와 인덱서를 구현해야 합니다!

 

신기하게도 인터페이스에 프로퍼티를 선언할 때 기존 클래스에서 선언하는 거랑 똑같지만 자동 구현 프로퍼티를 C# 컴파일러가 자동으로 구현해주지 않습니다. 인터페이스는 어떤 구현도 가지지 않기 때문이죠.

using System;

namespace PropertiesInInterface
{
    interface INamedValue
    {
        string Name { get; set; } // 자동으로 구현해주지 않습니다.
        string Value { get; set; }
    }

    class NamedValue : INamedValue
    {
        public string Name { get; set; } // 자동으로 구현해줍니다.
        public string Value { get; set; }
    }
    
    class Program
    {
        static void Main()
        {
            NamedValue name = new NamedValue()
            {
                Name = "이름",
                Value = "지코"
            };

            NamedValue height = new NamedValue()
            {
                Name = "키",
                Value = "181cm"
            };

            NamedValue weight = new NamedValue()
            {
                Name = "몸무게",
                Value = "67kg"
            };

            Console.WriteLine($"{name.Name} : {name.Value}");
            Console.WriteLine($"{height.Name} : {height.Value}");
            Console.WriteLine($"{weight.Name} : {weight.Value}");
        }
    }
}
이름 : 지코
키 : 181cm
몸무게 : 67kg

# 추상 클래스의 프로퍼티

추상 클래스도 앞서 공부한 인터페이스와 유사합니다.

다만 구현된 프로퍼티를 가질 수 있고, 인터페이스처럼 구현되지 않은 프로퍼티도 가질 수 있습니다.

 

인터페이스처럼 구현되지 않는 프로퍼티를 추상 프로퍼티라고 하며, 파생 클래스가 해당 프로퍼티를 구현하도록 강제하는 것입니다. 프로퍼티 앞에 abstract 키워드를 붙여야 합니다.

using System;

namespace PropertiesInAbstractClass
{
    abstract class Product
    {
        private static int serial = 0;
        public string SerialID
        {
            get { return String.Format("{0:d5}", serial++); }
        }

        abstract public DateTime ProductDate { get; set; } // 자동으로 구현 x
    }

    class MyProduct : Product
    {
        public override DateTime ProductDate { get; set; }
    }

    class Program
    {
        static void Main()
        {
            Product product_1 = new MyProduct()
            {
                ProductDate = new DateTime(2021, 12, 20)
            };

            Console.WriteLine("Product:{0}, Product Date:{1}", product_1.SerialID, product_1.ProductDate);

            Product product_2 = new MyProduct()
            {
                ProductDate = new DateTime(2021, 12, 21)
            };

            Console.WriteLine("Product:{0}, Product Date:{1}", product_2.SerialID, product_2.ProductDate);
        }
    }
}
Product:00000, Product Date:2021-12-20 오전 12:00:00
Product:00001, Product Date:2021-12-21 오전 12:00:00

 

'CS > C#' 카테고리의 다른 글

[C#] Record 와 Property  (0) 2021.12.21
[C#] property, 프로퍼티  (0) 2021.12.21
[C#] 인터페이스와 추상 클래스  (0) 2021.12.20
[C#] 정적 필드와 메서드(static)  (0) 2021.12.12
[C#] 생성자와 소멸자  (0) 2021.12.11

프로퍼티 두 번째 시간입니다.

 

# 프로퍼티란?

레코드에 들어가기 전에 참조 형식과 값 형식에 대해서 잠깐 언급하고 가보겠습니다.

값 형식은 필드가 많으면 많을수록 복사 비용이 커지게 됩니다. 왜냐하면 깊은 복사로 새 객체가 기존에 가진 모든 필드를 1:1 복사하기 때문이죠.

그렇지만 참조 형식에서는 이런 오버헤드가 없습니다. 객체가 참조하는 메모리 주소만 복사하면 되기 때문입니다. 하지만 참조는 얕은 복사를 진행하며, 깊은 복사가 필요한 경우 따로 프로그래머가 구현해줘야 합니다.

 

더 들어가봅시다.

값 형식은 객체를 비교할 때에도 기본적으로 내용을 비교하는데 모든 필드를 1:1 비교합니다. 불변 객체에서 필요한 방법입니다.

참조 형식은 내용 비교를 할 수 있으려면 프로그래머가 직접 비교 코드를 작성해야 합니다. 이때 object로부터 상속하는 Equals() 메서드를 오버 라이딩해서 구현합니다.

 

정리하면 불변 객체를 참조 형식으로 선언하면 함수 호출 인수나 컬렉션 요소를 사용할 때 복사 비용을 줄일 수 있습니다. 즉, 값 형식의 좋은 점과 참조 형식의 좋은 점을 쏙 체리 픽해서 가져와 사용합니다.

 

이것을 지원하는 것이 바로 오늘 배울 레코드입니다.

 

레코드 형식은 값 형식처럼 다룰 수 있는 불변 참조 형식(내부 상태를 변경할 수 없는 객체 = Immutable)으로, 참조 형식의 비용 효율 + 값 형식이 주는 편리함 모두를 제공합니다.

 

# 레코드 선언하기

record RTransaction
{
    public string From { get; init; }
    public string To { get; init; }
    public int Amount { get; init; }
}

기존 프로퍼티 사용 방법과 동일하지만 record 키워드를 사용합니다.

이렇게 선언한 레코드로 인스턴스(객체)를 만들면, 불변 객체가 만들어집니다.

 

레코드는 어디서에 빛을 발휘할까요?

복사할 때, 비교할 때 유용합니다.

 

# 레코드 복사하기

RTransaction tr2 = tr1 with { To = "Charlie" };
RTransaction tr3 = tr2 with { From = "Dave", Amount = 30 };

C# 컴파일러는 레코드 형식을 위한 복사 생성자를 자동으로 작성합니다. 왜?? 복사하기 위해서죠.

그런데 이 복사 생성자는 protected로 선언되어 있기 때문에 명시적으로 호출할 수 없습니다.

하지만 with 식을 이용한다면 우측의 레코드를 좌측으로 복사할 수 있으며 { } 안에 적은 프로퍼티를 변경해서 저장할 수 있습니다.

 

with 식은 객체 상태(프로퍼티)가 다양할수록 유용합니다.

인스턴스를 새로 할당할 때 모든 프로퍼티를 입력하지 않아도 되기 때문입니다.

using System;

namespace WithExp
{
    record RTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
        public override string ToString()
        {
            return $"{From,-10} -> {To,-10} : ${Amount}";
        }
    }

    class Program
    {
        static void Main()
        {
            RTransaction tr1 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };
            RTransaction tr2 = tr1 with { To = "Charlie" };
            RTransaction tr3 = tr2 with { From = "Dave", Amount = 30 };

            Console.WriteLine(tr1);
            Console.WriteLine(tr2);
            Console.WriteLine(tr3);
        }
    }
}
Alice      -> Bob        : $100
Alice      -> Charlie    : $100
Dave       -> Charlie    : $30

#레코드 객체 비교하기

C# 컴파일러는 레코드의 상태를 비교하는 Equals() 메서드를 자동으로 구현합니다.

기존 클래스 비교였다면 프로그래머가 직접 object로부터 상속받은 Equals() 메서드를 구현해야 했지만, 레코드는 C# 컴파일러가 자동으로 구현해주니까 단순이 선언만 하면 끝입니다.

 

아래의 코드는 프로그래머가 기존 클래스 비교에서 구현을 하지 않았다면 참조의 비교 형태와 레코드의 Equals()를 보여줍니다.

using System;

namespace RecordComp
{
    class CTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
        public override string ToString()
        {
            return $"{From,-10} -> {To,-10} : ${Amount}";
        }
    }

    record RTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
        public override string ToString()
        {
            return $"{From,-10} -> {To,-10} : ${Amount}";
        }
    }

    class Program
    {
        static void Main()
        {
            CTransaction trA = new CTransaction { From = "Alice", To = "Bob", Amount = 100 };
            CTransaction trB = new CTransaction { From = "Alice", To = "Bob", Amount = 100 };

            Console.WriteLine(trA);
            Console.WriteLine(trB);
            // 기본적으로 Equals는 참조를 비교하므로 따로 정의하지 않았다면 참조 비교로 인해 false
            Console.WriteLine($"trA equals to trB : {trA.Equals(trB)}");

            RTransaction tr1 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };
            RTransaction tr2 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };

            Console.WriteLine(tr1);
            Console.WriteLine(tr2);
            Console.WriteLine($"tr1 equals to tr2 : {tr1.Equals(tr2)}");
        }
    }
}
Alice      -> Bob        : $100
Alice      -> Bob        : $100
trA equals to trB : False
Alice      -> Bob        : $100
Alice      -> Bob        : $100
tr1 equals to tr2 : True

클래스 비교에서 Equals()를 오버 라이딩해서 구현하지 않았다면 참조(주소 값)만 비교하기 때문에 false가 나오게 됩니다. 하지만 레코드의 비교는 컴파일러가 자동으로 구현해주기 때문에 참조 비교가 아닌 진짜 값을 비교하므로 true입니다.

 

오늘은 record 에서 배웠습니다.

record와 프로퍼티는 어떤 점이 달랐는지 record의 장점과 왜 사용하는지에 대해서도 배웠습니다.

 

 

오늘은 프로퍼티에 대해서 배워보겠습니다.

기존에 C++을 공부를 했었고, 은닉성을 위해서는 public으로 필드 멤버를 선언하는 것이 아닌 private로 감추고

필요한 부분만 getter, setter를 구현해서 사용하라고 배웠습니다.

맞는 방법이지만 한 변수에 getter와 setter까지 하면 변수가 많아질수록 이 수 또한 엄청 많아지게 될 것입니다.

C#에서는 이러한 것들을 한 뭉치로 묶을 수 있는 프로퍼티를 제공합니다!

 

# 프로퍼티

class Program
{
    private int age;
    public int Age
    {
        get { return age; }
        set { age = value; }
    }
}

age라는 것을 Age라는 프로퍼티로 제공하게 됩니다.

Program p = new Program();
p.Age = 5; // 쉬운 setter
Console.WriteLine(p.Age); // 쉬운 getter

위와 같은 방식으로 쉽게 쓰고 대입할 수 있습니다.

 

좀 더 만들어봅시다.

using System;

namespace Property
{
    class BirthdayInfo
    {
        private string name;
        private DateTime birthday;

        public string Name // 프로퍼티!
        {
            get { return name; }
            set { name = value; }
        }

        public DateTime Birthday
        {
            get { return birthday; }
            set { birthday = value; }
        }

        public int Age
        {
            get
            {
                return new DateTime(DateTime.Now.Subtract(birthday).Ticks).Year;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            BirthdayInfo birth = new BirthdayInfo();
            birth.Name = "지코";
            birth.Birthday = new DateTime(1992, 9, 14);

            Console.WriteLine($"Name : {birth.Name}");
            Console.WriteLine($"Birthday : {birth.Birthday.ToShortDateString()}");
            Console.WriteLine($"Age : {birth.Age}");
        }
    }
}

# 자동 구현 프로퍼티 (Auto-Implemented-Property)

위의 프로퍼티도 매력적이지만 좀 더 줄일 수 있습니다.

class Program
{
    private int age;
    public int Age { get; set; }
}

이렇게 작성하면 단순히 age를 읽고 쓰는 기능이기 때문에, C# 컴파일러가 자동으로 프로퍼티를 구현해줍니다.

 

C# 7.0부터는 자동 구현 프로퍼티 선언과 동시에 초기화도 가능하게 지원합니다.

class Program
{
    private int age;
    public int Age { get; set; } = 10;
}

자동 구현 프로퍼티의 선언과 동시에 초기화하는 예제를 연습해봅시다.

using System;

namespace AutoImplementedProperty
{
    class BirthdayInfo
    {
        public string Name { get; set; } = "UnKnown";
        public DateTime Birthday { get; set; } = new DateTime(1, 1, 1);
        public int Age
        {
            get
            {
                return new DateTime(DateTime.Now.Subtract(Birthday).Ticks).Year;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            BirthdayInfo birth = new BirthdayInfo();
            Console.WriteLine($"Name : {birth.Name}");
            Console.WriteLine($"Birthday : {birth.Birthday.ToShortDateString()}");
            Console.WriteLine($"Age : {birth.Age}");

            birth.Name = "지코";
            birth.Birthday = new DateTime(1992, 9, 14);
            Console.WriteLine($"Name : {birth.Name}");
            Console.WriteLine($"Birthday : {birth.Birthday.ToShortDateString()}");
            Console.WriteLine($"Age : {birth.Age}");
        }
    }
}

 

# 자동 구현 프로퍼티의 뒷단

자동 구현 프로퍼티가 우리에게 편리함을 가져줍니다.

class Program
{
    //private int age; 이걸 쓰지 않아도 알아서 컴파일러가 뒷단에서 구현해줍니다.
    public int Age { get; set; } = 10;
}

기존에는 private로 age를 만들고 앞에 Age로 프로퍼티를 지원했지만, 그냥 Age 프로퍼티만 선언한다면 C# 컴파일러가 자동으로 구현해줍니다. 이것 때문에 우리는 좀 더 편안한 프로그래밍이 가능해집니다.

 

# 프로퍼티와 생성자

기존에 클래스는 클래스를 생성하면서 안에 멤버 필드를 초기화하고 싶다면 생성자를 만들어서 사용했습니다. 그런데 프로퍼티가 등장으로 객체를 생성할 때 각 필드를 초기화하는 또 다른 방법이 있습니다.

 

Name과 Birthday는 각각의 string과 DateTime의 프로퍼티입니다.

객체를 생성할 때 각각의 프로퍼티를 초기화하는 과정입니다.

초기화하고 싶은 프로퍼티만 사용할 수 있습니다.

BirthdayInfo birth = new BirthdayInfo()
{
    Name = "지코",
    Birthday = new DateTime(1992, 9, 14)
};
using System;

namespace ConstructorWithProperty
{
    class BirthdayInfo
    {
        public string Name { get; set; }
        public DateTime Birthday { get; set; }
        public int Age
        {
            get
            {
                return new DateTime(DateTime.Now.Subtract(Birthday).Ticks).Year;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            BirthdayInfo birth = new BirthdayInfo()
            {
                Name = "지코",
                Birthday = new DateTime(1992, 9, 14)
            };

            Console.WriteLine($"Name : {birth.Name}");
            Console.WriteLine($"Birthday : {birth.Birthday.ToShortDateString()}");
            Console.WriteLine($"Age : {birth.Age}");
        }
    }
}

 

# 초기화 전용(Init-Only) 자동 구현 프로퍼티

기존에 초기화한 필드를 다시 바꾸려는 것을 막는 키워드가 있었습니다.

예를 들어서 readonly처럼 한 번 초기화하면 다음 변경을 막아버리는 키워드가 그 예시입니다.

 

프로퍼티도 이러한 기능을 지원합니다.

    class Transaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }

        public override string ToString()
        {
            return $"{From,-10} -> {To,-10} : ${Amount}";
        }
    }

init 접근자는 set 접근자처럼 외부에서 프로퍼티 변경할 수 있지만 객체를 초기화할 때만 프로퍼티 변경이 가능하다는 점입니다. 이렇게 선언한 프로퍼티를 초기화 전용 자동 구현 프로퍼티라고 합니다.

using System;

namespace InitOnly
{
    class Transaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }

        public override string ToString()
        {
            return $"{From,-10} -> {To,-10} : ${Amount}";
        }
    }

    class Program
    {
        static void Main()
        {
            Transaction tr1 = new Transaction { From = "Alice", To = "Bob", Amount = 100 };
            Transaction tr2 = new Transaction { From = "Bob", To = "Charlie", Amount = 50 };
            Transaction tr3 = new Transaction { From = "Charlie", To = "Alice", Amount = 50 };

            Console.WriteLine(tr1);
            Console.WriteLine(tr2);
            Console.WriteLine(tr3);
        }
    }
}

각 트랜잭션은 객체를 생성할 때 각 프로퍼티를 초기화하고, 그 이후부터는 읽기 전용으로 사용하고 있습니다.

 

오늘은 프로퍼티가 왜 나오게 됐는지, 어떻게 사용하는지에 대해서 배웠습니다.

객체지향 프로그래밍의 '꽃'인 인터페이스와 추상 클래스에 대해서 공부했습니다.

배우기에 앞서서는 인터페이스와 추상 클래스가 많이 비슷하기도 하고 다른 점이 뭐가 있을까에 대해서 궁금했었는데 빠르게 알아봅시다!

 

# 인터페이스 선언

C#의 인터페이스는 다음과 같이 생겼습니다.

interface flyable
{
    void fly();
}

인터페이스에서는 메서드, 이벤트, 인덱서, 프로퍼티만을 가질 수 있고, 클래스의 선언과 비슷하지만 언뜻 보면 구현부가 없고 함수의 정의 부분만 있습니다.

인터페이스에서는 접근 제한 한정자를 사용할 수 없으며, 모든 것들이 public으로 선언됩니다.

클래스와는 다르게 인스턴스화를 만들 수 도 없고요.

 

다만, 인터페이스를 상속한 클래스에서는 인스턴스를 만드는 것이 가능합니다.

상속받은 클래스에서는 인터페이스에서 선언된 모든 메서드 및 프로퍼티를 구현해줘야하고, 이 메서드들은 public 한정자로 수식해야 합니다.

 

더 나아가면 인터페이스는 분명 인스턴스화를 하지 못하지만 그것을 상속받은 클래스에서는 인스턴스를 만들 수 있고 그것을 인터페이스가 참조할 수 있습니다.

이러한 것이 가능한 이유는 파생 클래스도 기반 클래스와 같은 형식으로 간주된다는 것에서부터 시작됩니다. 인터페이스를 상속받은 클래스의 관계에서도 동일합니다.

 

# 인터페이스는 왜 쓰는 것일까?

저도 항상 인터페이스하면 고개를 끄덕끄덕했지만 정작 왜 사용하는지에 대해서 의문이 많았습니다.

 

인터페이스를 한 마디로 정의하면 약속이라고 합니다.

인터페이스를 가지고 있는 클래스들은 인터페이스에서 정의한 모든 것들을 구현해야 한다는 약속을 가지고 있기 때문입니다.

 

또, 어떤 프로그램을 사용할 때 사용자의 입맛에 따라 결정을 해야한다고 할 때 인터페이스는 아주 훌륭한 해결책이 되기도 합니다.

 

인터페이스를 상속받는 객체는 인터페이스의 역할을 가지고 있기 때문에 인터페이스 명만 봐도 이 클래스가 어떤 지원을 하는지 대략적으로 알 수 도 있습니다.

 

아래에는 어떤 모니터에서 사용자로부터 입력받은 온도를 기록한다고 했을 때, logger가 어떻게 이 메시지를 기록할지 정해주는 방법입니다.

using System;
using System.IO;

namespace Interface
{
    interface ILogger // 인터페이스!
    {
        void WriteLog(string message);
    }

    // 나는 Console에 로그를 저장하겠다.
    class ConsoleLogger : ILogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine($"{DateTime.Now.ToLocalTime()}, {message}");
        }
    }

    // 나는 파일에 로그를 저장하겠다.
    class FileLogger : ILogger
    {
        private StreamWriter writer;

        public FileLogger(string path)
        {
            writer = File.CreateText(path);
            writer.AutoFlush = true;
        }

        public void WriteLog(string message)
        {
            writer.WriteLine($"{DateTime.Now.ToLocalTime()}, {message}");
        }
    }

    class ClimateMonitor
    {
        private ILogger logger; // 어떤 로거를 사용할 지
        public ClimateMonitor(ILogger logger)
        {
            this.logger = logger;
        }
        public void start()
        {
            while(true)
            {
                Console.WriteLine("온도를 입력 : ");
                string temperature = Console.ReadLine();
                if (temperature == "") break;

                // 등록된 로거에 message를 넘겨준다.
                logger.WriteLog($"현재 온도 : {temperature}");
            }
        }
    }

    class Program
    {
        static void Main()
        {
            ClimateMonitor monitor = new ClimateMonitor(new FileLogger("MyLog.txt"));

            monitor.start();
        }
    }
}

ClimateMonitor는 현재 온도를 기록하려고 하는데 기록하는 방법은 많습니다. 콘솔에 출력하거나 혹은 파일에 저장하거나 등등. 그럴 때 이러한 것을 프로그래머가 입맛에 따라 결정할 수 있게 내부 변수로 인터페이스를 가질 수 있도록 선언하고, 생성자를 통해 어떤 로거를 사용할지 결정한 후 그 로거를 실행하는 방법입니다.

 

중요한 것은 ConsoleLogger와 FileLogger는 모두 ILogger로부터 상속받았기 때문에 ILogger가 두 클래스를 참조할 수 있습니다.

 

어떤가요. 느낌이왔나요?

게임으로 비유하면, 같은 두손검이라도 '쟈드'와 '그륜힐'은 외형과 성능이 다릅니다. 그런데 캐릭터에 착용하면 똑같은 두손검입니다. 따라서 어떤 두손검을 사용할지 캐릭터 내부 변수로 ISward 라는 것을 만들고 캐릭터를 생성할 때 어떤 검을 끼워줄지 매핑하면 됩니다.

 

# 인터페이스도 상속할 수 있다?

인터페이스는 클래스는 물론 구조체도 인터페이스를 상속할 수 있습니다.

그런데 문제점이 발생합니다.

 

기존에 상속받아서 사용하고 있었는데 인터페이스 내부를 수정하려고 한다면? 펑;;;;

왜냐하면 인터페이스를 상속받은 것들은 모두 그 안에 있는 것을 구현해줘야 하기 때문입니다.

 

따라서 기존의 소스 코드에 영향을 주지 않고도 새로운 기능을 추가하기 위해서는 인터페이스를 상속하는 인터페이스를 이용하는 것이 좋다고 합니다. (프로젝트가 작으면 문제없지만, 방대하다면 어느 세월에 고칠까요? 게다가 고친다고 해도 사이드 이펙트는??? => 야근)

 

또 상속하려는 인터페이스가 소스 코드가 아닌 어셈블리만으로 제공되는 경우 우리가 내부 인터페이스를 수정할 수 없는 경우도 있습니다.

 

기존에 ILogger를 상속받는 IFormattableLogger는 다음과 같이 작성됩니다.

interface IFormattableLogger : ILogger
{
    void WriteLog(string format, params object[] args);
}

매개변수로 기존 메시지와, 어떤 인자를 받을지 모르므로 모든 객체의 최상 부모인 object 형식으로 받고 있습니다.

 

직접 확인해봅시다!

using System;

namespace DerivedInterface
{
    // 기존에 있던 인터페이스 입니다.
    interface ILogger
    {
        void WriteLog(string message);
    }

    // 추가 기능이 생겼습니다. (다양한 인자를 받고 싶어요!)
    interface IFormattableLogger : ILogger
    {
        void WriteLog(string format, params Object[] arges);
    }

    class ConsoleLogger : IFormattableLogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine($"{DateTime.Now.ToLocalTime()}, {message}");
        }

        public void WriteLog(string format, params Object[] args)
        {
            // 지정된 형식에 따라 개체의 값을 문자열로 변환합니다.
            String message = String.Format(format, args);
            Console.WriteLine($"{DateTime.Now.ToLocalTime()}, {message}");
        }
    }

    class Program
    {
        static void Main()
        {
            IFormattableLogger logger = new ConsoleLogger();
            logger.WriteLog("C# is good language");
            logger.WriteLog("{0} + {1} + {2} = {3}", 1, 2, 3, 6);
        }
    }
}

 

# 인터페이스는 다중 상속이 가능해?

C#에서는 여러 클래스를 한꺼번에 상속할 수 없습니다.

어떤 클래스가 A로부터 상속받고, B로부터 상속받았는데 신기하게도 A에도 charge()가 있고, B에도 charge()라는 메서드가 있습니다. 그래서 어떤 charge()를 사용할지 모호하기 때문에 이것을 죽음의 다이아몬드라고 합니다.

 

컴퓨터 세계에서는 모호한 프로그램을 재앙이라고 합니다. 문법의 사소한 한 톨이라도 틀리면 용납하지 않으니까요.

 

그런데 신기하게도 인터페이스는 다중 상속을 지원합니다.

이것도 쓸 수 있고 저것도 쓸 수 있습니다. 이게 가능한 이유는 인터페이스의 경우 외형만 물려주기 때문에 속이 어떨지 몰라도 겉모습만큼은 확실하게 같아야 합니다. 따라서 죽음의 다이아몬드 문제도 생기지 않습니다.

 

다중 상속의 인터페이스 확인해봅시다!

using System;

namespace MultiInterfaceInheritance
{
    interface IRunable
    {
        void Run(); // 나를 받으면 달릴 수 있어
    }

    interface IFlyable
    {
        void Fly(); // 나를 받으면 날 수 있어
    }

    class FlyingOrc : IRunable, IFlyable
    {
        public void Run()
        {
            Console.WriteLine("난 오크 뛴다!");
        }
        public void Fly()
        {
            Console.WriteLine("난 오크 날기도 한다!");
        }
    }

    class Program
    {
        static void Main()
        {
            FlyingOrc orc = new FlyingOrc();
            orc.Run();
            orc.Fly();

            IRunable runnable = orc as IRunable;
            runnable.Run();

            IFlyable flyable = orc as IFlyable;
            flyable.Fly();
        }
    }
}

오크는 뛰기와 날기라는 인터페이스를 모두 가지고 있기 때문에 둘 다 사용할 수 있습니다. 즉 다중 인터페이스 상속이 가능하게 되었습니다. 무서운 오크죠.

 

# 인터페이스의 기본 구현 메서드

기존까지 알고 있었던 인터페이스는 구현부를 만들 수 없었습니다. 그런데? 가능하게 할 수 있습니다.

 

가능하게 하기 전에 왜 필요할 지부터 보겠습니다.

초기 버전을 설계할 때는 이렇게 사용하도록 인터페이스를 정의하고 서비스해왔는데 다시 보니까 이 기능이 빠져있었던 것입니다. 그래서 개발자는 그 인터페이슬 수정하려고 했으나 아차!.. 그것을 상속받아 사용하는 개체가 +99개인 겁니다.. 어떻게 하면 안전하게 추가할 수 있을까요?

 

여기서 등장한 것이 바로 기본 구현 메서드입니다.

인터페이스를 수정했지만 다른 기존 코드에는 아무런 영향을 받지 않습니다.

게다가 인터페이스의 기본 구현 메서드는 인터페이스 참조로 업 캐스팅 했을 때만 사용할 수 있기 때문에 프로그래머가 파생 클래스에서 인터페이스에 추가된 메서드를 엉뚱하게 호출할 일도 없습니다.

 

살펴봅시다.

using System;

namespace DefaultImplemetation
{
    interface ILogger
    {
        void WriteLog(string message);
        void WriteError(string error)
        {
            WriteLog(error); // 구현부가 있네요!
        }
    }

    class ConsoleLogger : ILogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine($"{message}");
        }
    }

    class Program
    {
        static void Main()
        {
            ILogger logger = new ConsoleLogger();
            // 업캐스팅일 때 다 사용할 수 있습니다!
            logger.WriteLog("System Up");
            logger.WriteError("Error!!");

            // 프로그래머가 엉뚱한 호출을 사용을 못하게 막습니다.
            ConsoleLogger clogger = new ConsoleLogger();
            clogger.WriteLog("System Up");
            //clogger.WriteError("난 왜 안돼?");
        }
    }
}

 

# 추상 클래스는 인터페이스와 클래스 사이

추상 클래스는 인터페이스와 다르게 '구현'을 할 수 있습니다.

그렇지만 클래스와는 다르게 인스턴스화 및 인스턴스를 가질 수 없습니다.

=> 구현을 할 수 있지만 인스턴스를 만들지 못합니다.

인터페이스는 모든 메서드가 public이지만 클래스는 default가 private입니다.

추상 클래스는 추상 메서드를 가질 수 있습니다. 이것 때문에 인터페이스와도 유사합니다.

추상 메서드는 구현을 못하지만 파생 클래스에서는 반드시 구현하도록 강제가 됩니다. (인터페이스와 유사)

 

추상 클래스를 이용한 프로그램을 살펴봅시다.

using System;

namespace AbstractClass
{
    abstract class AbstractBase
    {
        protected void PrivateMethodA()
        {
            Console.WriteLine("AbstractBase.PrivateMethodA()");
        }

        public void PublicMethodA()
        {
            Console.WriteLine("PublicMethodA()");
        }

        // 추상 메서드
        public abstract void AbstractMethodA();
    }

    class Derived : AbstractBase
    {
        // 구현하기!
        public override void AbstractMethodA()
        {
            Console.WriteLine("Derived.AbstractMethodA()");
            // 상속받았기 때문에 사용할 수 있습니다.
            PrivateMethodA();
        }
    }

    class Program
    {
        static void Main()
        {
            AbstractBase obj = new Derived();
            obj.AbstractMethodA();
            // 상속받았기 때문에 사용할 수 있습니다.
            obj.PublicMethodA();
        }
    }
}

추상 클래스는 일반 클래스가 가질 수 있는 구현 + 추상 메서드를 가지고 있습니다. (인터페이스 역할)

 

추상 메서드는 추상 클래스를 사용하는 프로그래머가 그 기능을 정의하도록 강제하는 장치이기 때문에 혹여나 실수를 하더라도 컴파일러가 이를 상기시켜줄 수 있습니다. 그래서 추상 클래스를 사용합니다.

 

길고 길었지만,

- 인터페이스를 왜 사용하는지?

- 인터페이스와 클래스의 차이

- 인터페이스와 추상 클래스의 차이

에 대해서 배웠습니다.

'CS > C#' 카테고리의 다른 글

[C#] Record 와 Property  (0) 2021.12.21
[C#] property, 프로퍼티  (0) 2021.12.21
[C#] 정적 필드와 메서드(static)  (0) 2021.12.12
[C#] 생성자와 소멸자  (0) 2021.12.11
[C#] 클래스  (0) 2021.12.11

C#에 오면서 헷갈리는 부분이 바로 정적 필드와 메서드 부분이었습니다.

아마도 이쪽 부분에 대해서 개념이 흔들려서 그런 것 같기도 했으나 오늘 공부로 한 층 더 업그레이드해보겠습니다.

# static

메서드나 필드가 클래스의 인스턴스가 아닌 클래스 자체에 소속되도록 지정하는 한정자라고 합니다.

 

하나의 프로그램에는 인스턴스는 여러 개가 존재할 수 있으나, 붕어빵을 만드는 틀(클래스)은 단 하나만 존재합니다.

어떤 필드가 클래스에 소속된다는 것은 그 필드가 프로그램 전체에서 유일하게 존재한다는 것을 의미합니다.

 

유심히 살펴보면 Main 함수도 static으로 선언되어 있습니다.

 

말이 어렵죠.

일반적으로 만드는 인스턴스와 static을 비교해보겠습니다.

# 정적 필드

using System;

class Instance
{
    public int a;
}

class Static
{
    public static int a;
}

class Program
{
    static void Main()
    {
        // 일반적으로는 객체를 찍어낸다음 사용합니다.
        Instance obj = new Instance();
        obj.a = 1;

        // 인스턴스를 만들지 않고 클래스의 이름을 통해 필드에 직접 접근합니다.
        Static.a = 1;
    }
}

저도 처음에 이 개념이 와닿지가 않았습니다. 작성하는 지금도 그렇고요...

 

정적 필드를 만들어서 얻는 이득은 무엇일까요?

프로그램 전체에 걸쳐 공유해야 하는 변수가 있다면 정적 필드를 이용하면 된다고 합니다.

예를 들어 우리가 지금까지 붕어빵을 몇 개나 찍어서 판매했는지 수량을 체크하고자 할 때, 수량을 static으로 선언해서 관리한다면 편하겠죠?

# 정적 메서드

정적 메서드 역시 클래스의 인스턴스를 생성하지 않고도 호출이 가능한 메서드입니다.

 

객체의 내부 데이터를 이용해서 사용하는 경우는 인스턴스 메서드를 사용하고, 내부 데이터를 이용할 일이 없는 경우에는 별도의 인스턴스 생성 없이 호출할 수 있도록 메서드를 정적으로 선언합니다.

 

오늘은 정적 필드와 메서드에 대해서 알아봤습니다.

어떻게 사용하냐에 따라 갈리겠지만..

게임 개발에서는 생각나는 방법이 자주 사용되는 메서드의 경우나 유틸리티 함수 혹은 싱글턴 패턴을 이용할 때 사용하는 것 같습니다.

 

'CS > C#' 카테고리의 다른 글

[C#] property, 프로퍼티  (0) 2021.12.21
[C#] 인터페이스와 추상 클래스  (0) 2021.12.20
[C#] 생성자와 소멸자  (0) 2021.12.11
[C#] 클래스  (0) 2021.12.11
[C#] 문자열 찾기 함수들  (0) 2021.12.10

객체를 생성하고 만드는 과정에서 사용되는 생성자와, 언젠간 객체도 사용되지 않는다면 소멸하므로 소멸할 때 사용하는 소멸자에 대해서 알아보겠습니다.

# 생성자

이름 그대로 생성할 때 호출되는 녀석입니다.

생성자의 임무는 클래스의 객체를 생성하는 것이며, 동시에 스페셜하게 readonly도 초기화가 가능합니다.

class Name
{
    public int a;
    public int b;

    public Name()
    {
        // 디폴트 생성자
    }

    public Name(int a, int b)
    {
        // 매개변수를 입력받아 원하는 값으로 초기화
        this.a = a;
        this.b = b;
    }
}

클래스를 선언할 때 명시적으로 생성자를 구현하지 않아도 객체가 생성이 됩니다. 그 이유는 컴파일러에게 디폴트 생성자를 만들어주기 때문입니다.

 

디폴트 생성자를 지원하더라도, 객체를 생성하는 시점에서 맛있는 붕어빵을 만들 때 물가로 인해 가격이 상승해서 만들 때 가격을 올린다거나, 맛을 바꾼다거나, 이름을 바꾸는 등 사용자가 원하는 필드의 값으로 바꾸고 싶은 경우가 있습니다.

 

그럴 때 매개변수를 입력받아 원하는 값으로 초기화를 할 수 있도록 생성자를 오버 로딩이 가능합니다.

오버 로딩이름은 동일하지만 매개변수의 다양한 유형의 호출에 응답할 수 있게 작성할 수 있습니다.

 

생성자클래스의 필드를 초기화할 수 있는 최적의 장소라고 할 수 있습니다.

 

다만 주의할 점은 프로그래머가 생성자를 하나라도 직접 정의한다면 C# 컴파일러는 매개변수가 없는 디폴트 생성자를 제공하지 않기 때문에 디폴트 생성자도 사용한다면 직접 정의해야 합니다.

# 소멸자(종료자)

객체도 언젠간 우리처럼 죽는 날이 옵니다.

그러면 그 객체가 가지고 있던 자원을 모두 반납하고 하늘로 가겠죠. 하늘로 가기 전에 자원을 반납하거나 뒷정리를 하는 공간입니다.

 

신기한 게 소멸자는 생성자와 달리 매개변수도 없고, 한정자(public, private, protected)도 사용하지 않습니다.

또한 같은 이름의 유형을 다르게 하여 지정하는 오버 로딩도 지원하지 않습니다.

 

이 소멸자는 CLR의 가비지 컬렉터가 객체를 소멸되는 시점을 판단해서 호출합니다. (사용자가 호출할 수 없습니다!)

~Name()
{
    Console.WriteLine("바이바이 세상");
}

C++에서는 동적으로 할당된 메모리를 해제하는 공간에서 사용하는 개념이지만, C#에서는 가급적 사용하지 않는 것이 좋습니다.??

 

C#은 C++과 다르게 메모리 관리를 GC라는 가비지 컬렉터가 대신해줍니다. 그래서 편리하지만, CLR의 가비지 컬렉터가 언제 동작할지 예측할 수 없다는 것이 단점입니다.

 

자세한 방법은 추후에 작성할 것이지만 간단하게 말하면, GC는 쓰레기가 생긴다고 바로 치우지 않습니다.

어느 정도 양에 이르러야 동작합니다. 그래서 문제는 쓰레기가 차오르는 시간을 정확하게 알 수 없을뿐더러 가비지 컬렉터의 동작할 시점도 알 수 없기 때문에 소멸자에서 우리가 원하는 행동을 객체가 소멸된다고 해서 바로 얻어올 수 없다는 것이 문제가 됩니다.

 

소멸자를 명시적으로 구현하면, 가비지 컬렉터는 클래스의 족보를 타고 올라가 객체로부터 상속받은 Finalize() 메서드를 호출하게 되는데 이때 응용 프로그램의 성능 저하를 초래할 확률이 높아 권장하지 않는다고 합니다.

 

또, 생각보다 CLR의 가비지 컬렉터는 사용자보다 더 똑똑하게 객체의 소멸을 처리하고 있기 때문에

사용자는 생성자만 신경 쓰고, 뒤처리는 GC에게 맡기는 것이 좋습니다.

 

!!! 정말 중요한 점은 객체가 소멸될 때 어떤 순서로 소멸시킬지에 대한 보장이 없고 이는 매 실행마다 달라질 수 있다는 것입니다. (먼저 소멸되더라도 나중에 소멸된 객체보다 더 늦게 처리될 수 있다는 뜻)

 

오늘은 코드에 중점적이기보다는 생성자와 소멸자가 C#에서는 어떻게 사용되고 있으며, 객체의 소멸과정은 GC라는 가비지 컬렉터가 수행하고 있다는 점을 알았습니다.

'CS > C#' 카테고리의 다른 글

[C#] 인터페이스와 추상 클래스  (0) 2021.12.20
[C#] 정적 필드와 메서드(static)  (0) 2021.12.12
[C#] 클래스  (0) 2021.12.11
[C#] 문자열 찾기 함수들  (0) 2021.12.10
[C#] var  (0) 2021.12.10

프로그래머는 추상주의 예술가라고 합니다.

세상에 존재하거나 존재하지 않는 것을 코드로 나타내야 하며 컴퓨터 세계에서 실체화합니다.

객체로 표현하는 방법이 객체지향 프로그래밍입니다.

# 객체

객체란 것은 세상의 모든 것을 지칭하는 단어입니다. 자동차, 연필, 붕어빵 등등

객체의 주요 특징은 속성과 기능이 있습니다.

속성(property) : 데이터

기능(function) : 행동

 

예를 들어 사람이라는 객체는 속성은 나이, 주민번호, 몸무게, 키가 될 수 있고, 기능은 걷기, 뛰기, 달리기, 먹기와 같이 나열할 수 있습니다.

 

이러한 객체를 어떻게 만들 수 있을까요? 바로 클래스를 이용해서 만들 수 있습니다.

# 클래스

클래스객체를 만들기 위한 청사진(틀)입니다.

붕어빵이라는 객체를 만들기 위해서는 붕어빵 틀이 있어야 찍어낼 수 있습니다. 

붕어빵 기계가 클래스, 붕어빵이 객체입니다.

 

주의할 점은 클래스라는 자체는 (속성과 기능)을 가지는 틀이지만 실제로 존재하는 객체는 아닙니다.

클래스를 이용해서 만든 객체가 바로 실체죠.

어떤 부분으로는 객체가 인스턴스(실체)라고도 불립니다.

 

클래스는 복합 데이터 형식이다.

기본 데이터 형식을 조합해서 만들며, 기능들(메서드)를 묶는 집합입니다.


설명이 엄청 길어졌는데, 이제부터 진짜 클래스를 C#에서는 어떻게 만드는지 알아보겠습니다.

# 클래스의 선언

class FishBread
{
    // 필드(속성)
    // 메소드(기능)
}

클래스에 아까 속성과 기능이 있다고 했습니다.

속성은 보통 데이터로 표현될 수 있는 곳을 적고, 기능은 보통 그 객체가 하는 행동들에 대해서 적는 곳입니다.

이렇게 선언된 속성과 기능들은 클래스 내에 선언됐다고 해서 멤버(Member)라고도 부릅니다.

# 객체의 생성

하지만 위의 클래스로는 어떠한 객체를 만들 수 없습니다.

클래스는 객체를 만들기 위한 틀일 뿐, 실제 객체(인스턴스)가 아니므로 아래와 같이 찍어내야 합니다.

FishBread bread1 = new FishBread(); // 아무 옵션이 없는 붕어빵

그냥 옵션이 없는 붕어빵을 찍어내면 밋밋한 밀가루 빵일 것입니다.

그래서 우리는 좀 더 색다른 붕어빵을 찍기 위해서는 생성자(Constructor)를 이용해서 맛있는 붕어빵을 만들 수 있습니다.

# 생성자와 new 연산자

public FishBread(string name, string taste, int price)
{
    this.name = name;
    this.taste = taste;
    this.price = price;
}

FishBread bread = new FishBread("일반 붕어빵", "팥", 1000);

보통 아무것도 지정하지 않으면 디폴트 생성자(인자가 없는)가 만들어지며, 프로그래머가 위와 같이 이름, 맛, 가격을 정의한 생성자를 만든다면 디폴트 생성자는 프로그래머가 따로 만들어줘야 하지만, 이렇게 하면 맛있는 붕어빵을 만들 수 있게 됩니다.

 

생성자와 new 연산자를 사용해서 맛있는 붕어빵을 만들 수 있습니다.

이때 생성한 객체는 힙에 생성이 되고, bread는 힙에 생성한 객체를 가리키게 됩니다.

 

이제 전체적으로 붕어빵 틀을 만들고 붕어빵을 만들어봅시다.

using System;
class FishBread
{
    // 속성들 : 데이터
    public string name;
    public string taste;
    public int price;

    // 생성자
    public FishBread(string name, string taste, int price)
    {
        this.name = name;
        this.taste = taste;
        this.price = price;
    }

    // 기능들 : 메소드
    public void PrintFishBread()
    {
        Console.WriteLine($"name:{name}, taste:{taste}, price:{price}");
    }
}

class Program
{
    static void Main()
    {
        FishBread bread = new FishBread("일반 붕어빵", "팥", 1000);
        bread.PrintFishBread();
    }
}

[실행결과]

name:일반 붕어빵, taste:팥, price:1000

 

클래스에 대해서 생성하는 방법과 클래스와 객체의 의미에 대해서 공부했습니다.

'CS > C#' 카테고리의 다른 글

[C#] 정적 필드와 메서드(static)  (0) 2021.12.12
[C#] 생성자와 소멸자  (0) 2021.12.11
[C#] 문자열 찾기 함수들  (0) 2021.12.10
[C#] var  (0) 2021.12.10
[C#] foreach  (0) 2021.12.05

항상 문자열의 함수는 자주 사용하지 않거나 경험이 적으면 까먹는 경우가 종종 있습니다.

매번 서치 해서 보는 것도 좋지만.. 따로 정리를 해두면 어떨까 해서 자료들을 모아 정리해보겠습니다.

 

C#에서는 string에 대해서 여러 가지 다양한 문자열 찾기 함수를 지원하고 있습니다.

 

# IndexOf() : 현재 문자열에서 찾고자 하는 지정된 문자 혹은 문자열의 위치를 앞에서부터 찾아서 반환합니다.

해당하는 부분이 없으면(존재하지 않으면) -1을 리턴합니다.

중요한 점은 맨 처음 발견되는(시작되는) 인덱스를 보고한다는 점입니다.

string str = "Hello, World!";

int a = str.IndexOf('W');    // 7
int b = str.IndexOf("rld!"); // 9
int c = str.IndexOf("zee");  // -1

맨 처음이 싫다면 직접 인덱스를 지정해서 그 부분부터 탐색할 수 도 있습니다.

IndexOf(String, Int32, Int32)
public int IndexOf (string str, int startIndex, int count);

str : 검색할 문자열
startIndex : 검색을 시작할 위치
count : 검사할 문자 위치의 수

# LastIndexOf() : 현재 문자열에서 찾고자 하는 지정된 문자 혹은 문자열의 위치를 뒤에서부터 찾아서 반환합니다.

맨 마지막부터 훓는 방식도 있습니다.

재밌는 점은 결과가 같습니다. 우리가 찾고자 하는 시작 인덱스를 반환하기 때문입니다.

string str = "Hello, World!";

int a = str.LastIndexOf('W');    // 7
int b = str.LastIndexOf("rld!"); // 9
int c = str.LastIndexOf("zee");  // -1

# StartsWith() : 현재 문자열이 지정된 문자열로 시작하는지를 평가합니다.

여기서 평가는 있으면 true고, 없으면 false입니다.

헷갈릴 수 있는데 현재 문자열로 시작하지 않으면 false입니다.

string str = "Hello, World!";

Console.WriteLine($"StartsWith 'Hello' : {str.StartsWith("Hello")}");   // true
Console.WriteLine($"StartsWith 'World!' : {str.StartsWith("World!")}"); // false

# EndsWith() : 현재 문자열이 지정된 문자열로 끝나는지를 평가합니다.

string str = "Hello, World!";

Console.WriteLine($"EndsWith 'Hello' : {str.EndsWith("Hello")}");   // false
Console.WriteLine($"EndsWith 'World!' : {str.EndsWith("World!")}"); // true

# Contains() : 현재 문자열이 지정된 문자열을 포함하는지를 평가합니다.

많이 쓰이는 메서드입니다. 포함하면 true

string str = "Hello, World!";

Console.WriteLine(str.Contains(","));       // true
Console.WriteLine(str.Contains(", W"));     // true
Console.WriteLine(str.Contains("World!!")); // false
Console.WriteLine(str.Contains("llo"));     // true

# Replace() : 현재 문자열에서 지정된 문자열로 대체되며, 대체된 새 문자열을 리턴합니다.

많이 쓰는 메서드입니다. (대체된다 라고 생각하면 편합니다.)

string str = "Hello, World!";
string replaceStr = str.Replace("o", "z"); 

Console.WriteLine($"old:{str}");         // old:Hello, World!
Console.WriteLine($"new:{replaceStr}"); // new:Hellz, Wzrld!

 

[ 정리하기 ]

위의 6가지 메서드를 살펴봤습니다.

공통점은 문자열에서 "내가 원하는 부분"을 찾아주는 기능뿐만 아니라 평가나 대체도 가능하다는 점입니다.

'CS > C#' 카테고리의 다른 글

[C#] 생성자와 소멸자  (0) 2021.12.11
[C#] 클래스  (0) 2021.12.11
[C#] var  (0) 2021.12.10
[C#] foreach  (0) 2021.12.05
[C#] null 병합 연산자, ??  (0) 2021.12.05

[ var ]

var에 대해서 정리해봅시다!

C++에서도 'auto'라는 키워드가 있었는데, C#에서도 'auto'와 비슷한 기능을 하는 'var'가 존재합니다.

var a = 3;
var b = "string";

var를 사용해서 변수를 선언하면 컴파일러가 자동으로 해당 변수의 형식을 지정해줍니다.

이러한 키워드는 왜 사용하는지 항상 의문이 들었는데, 변수의 형식이 긴 경우에 간단하게 해 준다는 장점이 있습니다.

C++에서는 iterator 같은 형식을 지정할 때 생각보다 길게 되는데 이러한 형식을 줄여주는 것처럼 말이죠.

vector<int> v;
vector<int>::iterator iter = v.begin();

// 간단해진 타입명
auto iter = v.begin();

vector<pair<pair<int, int>, double> > v2;
for(auto iter = v2.begin(); iter != v2.end(); iter++)
{

}

C#도 비슷하게 축약이 가능합니다.

class Program
{
    static void Main()
    {
        Dictionary<int, List<int>> dict = new Dictionary<int, List<int>>();

        List<int> a = new List<int>();
        a.Add(1);
        a.Add(2);

        List<int> b = new List<int>();
        a.Add(3);
        a.Add(4);

        dict.Add(1, a);
        dict.Add(2, b);

        foreach (var list in dict)
        {
            foreach (var num in list.Value)
            {
                Console.WriteLine($"Key:{list.Key} Value:{num}");
            }
        }
    }
}

이렇게 편리한 var도 주의할 점이 있습니다.

# var를 이용해서 변수를 선언하려면 선언과 동시에 초기화가 필요합니다.

그래야지 컴파일러가 데이터를 보고 형식을 추론하여 지정하기 때문이죠.

 

# var는 지역 변수로만 사용할 수 있습니다.

가만히 생각해보면, 클래스의 필드를 선언할 때는 반드시 명시적 형식을 선언해야 합니다.

생성자를 통해서 초기화를 하는 부분이 대부분이기 때문에 만약에 var로 선언하면 컴파일러가 무슨 형식인지 알 수 없기 때문입니다.

 

[ 연습해보기 ]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        var a = 20;
        Console.WriteLine($"Type:{a.GetType()} Value:{a}");

        var b = 3.141592;
        Console.WriteLine($"Type:{b.GetType()} Value:{b}");

        var c = "Programming C#";
        Console.WriteLine($"Type:{c.GetType()} Value:{c}");

        var d = new int[] { 10, 20, 30 };
        Console.WriteLine($"Type:{d.GetType()} Value:{d}");
        foreach(var e in d)
        {
            Console.Write($"{e} ");
        }
        Console.WriteLine();
    }
}

[ 실행 결과 ] 

Type:System.Int32 Value:20
Type:System.Double Value:3.141592
Type:System.String Value:Programming C#
Type:System.Int32[] Value:System.Int32[]
10 20 30

'CS > C#' 카테고리의 다른 글

[C#] 클래스  (0) 2021.12.11
[C#] 문자열 찾기 함수들  (0) 2021.12.10
[C#] foreach  (0) 2021.12.05
[C#] null 병합 연산자, ??  (0) 2021.12.05
[C#] IsNullOrEmpty  (0) 2021.11.19

이번에는 foreach문에 대해서 공부해보겠습니다.

foreach 문이 왜 등장했을까요? 기존에 for, while, do~while 문으로 대체가 가능한데..

 

그러다가 어떤 한 블로그의 말을 듣고 납득이 되었습니다.

그 말은 foreach문index범위를 벗어나는 위험성이 없다는 것입니다.

무슨말이냐면, 배열이나 컬렉션의 끝에 도달하면 자동으로 반복이 종료하게 되는데요.

만약에 foreach문을 사용하지 않고 기존에 알고 있었던 for문을 이용해서 구현했더라면 프로그래머의 실수로 인해 할당되지 않은 영역을 참조하는 경우가 발생할 수 있습니다.

이러한 경우가 바로 IndexOutOfRangeException 입니다.

정확히는 해당 범위 외부에 있는 인덱스를 사용해서 배열 또는 컬렉션의 요소에 액세스하려고 할 때 발생하는 오류입니다.

 

가끔 개발을 하다보면 나도 모르게 index범위를 잘못 설정하여 프로그램이 오류로 인해 돌아가지 않는 경우가 빈번합니다.

큰 개발일수록 이러한 작은 실수를 줄이기 위해서 등장한 것이 바로 foreach문입니다.

 

다만 foreach문도 단점이 있습니다.

다른 반복문에 비해 3~4배 정도 느리다고 합니다.

하지만 이러한 문제 때문에 짜잘한 부분에서 퍼포먼스를 줄이는 것보다는 병목현상이 발생하는 부분을 찾아서 개선하는 것이 전체 프로젝트의 퍼포먼스를 올린다는 것을 명심해야 합니다. 게다가 사람은 언제나 실수를 하기 때문이죠.

 

주저리 주저리 설명이 길었는데 이제 foreach문을 어떻게 사용하는지 알아봅시다.

foreach(데이터타입 변수명 in 배열(컬렉션)
{
     // to-do
}

배열 혹은 컬렉션의 요소를 차례대로 순회하면서 in 키워드 앞에 있는 변수에 담아줍니다.

다만 박싱(언박싱) 부분만 주의해서 사용한다면 좋을 것 같습니다.

using System;

class Program
{
    static void Main(string[] args)
    {
        int[] arr = new int[] { 1, 2, 3, 4, 5, 6 };

        foreach(int n in arr)
        {
            Console.WriteLine(n);
        }
    }
}
1
2
3
4
5
6

 

'CS > C#' 카테고리의 다른 글

[C#] 클래스  (0) 2021.12.11
[C#] 문자열 찾기 함수들  (0) 2021.12.10
[C#] var  (0) 2021.12.10
[C#] null 병합 연산자, ??  (0) 2021.12.05
[C#] IsNullOrEmpty  (0) 2021.11.19

+ Recent posts