CS/Effective C#

캐스트보다는 is, as가 좋다

Mirab 2023. 5. 6. 22:54

캐스트의 경우에는 스타크래프트로 따지면, 드랍쉽에는 여러 유닛을 태울 수 있는데요. 여러 유닛의 각 특징들의 메서드(행동)를 가져오려고 할 때 그 유닛으로 캐스팅을 해야 그 유닛의 고유의 능력이나 행동을 가져올 수 있습니다.

그럴 때 캐스트가 필요하게 되는데요.

캐스트에 대해서 어떻게 하면 더 효율적으로 사용할 수 있는지에 대해서 알아봅시다.

 

C#은 정적 타이핑을 수행하는 언어입니다.

그래서 보통 판단은 정적으로 하려고 하는데요. 위와 같은 상황에서 런타임에서(게임 실행 중)에 반드시 타입을 확인해야할 필요가 있는 경우가 있습니다.

 

C#에서는 2가지 캐스트를 지원합니다.

1. as 연산자를 사용하는 방법 (더 방어적으로 사용하는 is 연산자로 형변환이 가능한지를 확인한 후에 실제 형변환 진행)

2. 일반적으로 알고 있는 컴파일러의 캐스트 연산자 구문을 사용하는 방법

 

형변환을 수행하는 경우에는 캐스팅을 사용하기보다는 as 연산자를 추천합니다.

- 안전한다.

- 런타임에 더 효율적으로 동작한다.

 

다만 as 나 is 연산자는 객체의 타입이 변환하려는 타입과 정확히 일치할 경우에만 형변환이 성공적으로 수행됩니다.

일반적으로는 형변환 과정에서 새로운 객체가 생성되는 경우는 없지만,

예외적으로 as 연산자를 이용하여 박싱된 값 타입의 객체를 nullable 값 타입의 객체로 변환하는 경우에 새로운 객체가 생성됩니다.

object o = Factory.GetValue();
var i = o as int?;
if (i != null)
    Console.WriteLine(i.Value);

위의 경우에는 as 연산자를 사용할 때 int는 값 타입이고 null이 될 수 없어서 일반적으로는 int로 형변환이 불가능하지만,

as 연산자는 그대로 사용하면서 nullable 타입으로 형변환을 수행한 후에 그 값이 null인지 판단으로 확인하는 방법도 있다.

 

as 연잔자를 사용해서 캐스팅 방법은 다음과 같다.

 object o = Factory.GetObject();

 MyType t = o as MyType;

 if (t != null)
 {
     // MyType 객체 사용
 }
 else
 {
     // 실패
 }
as 연산자와 캐스팅의 가장 큰 차이

사용자 정의 형변환을 어떻게 다루는가 하는 점입니다.

 

as나 is연산자는 런타임에 객체의 타입을 확인하고 필요에 따라서 박싱을 수행하는 것을 제외하고는 어떠한 작업도 수행하지 않습니다. 임의의 객체를 다른 타입으로 형변환하려면 이 객체는 지정한 타입이거나 혹은 지정한 타입을 상속한 타입이어야만 하고 그 외에는 null로 지정됩니다.

 

캐스팅의 경우 객체를 지정한 타입으로 변환하기 위해서 형변환 연산자가 개입될 수 있습니다.

대표적인 형변환 연산자가 바로 숫자 타입에 대한 형변환 연산자라면 (long 타입을 short 타입으로 캐스팅하면 일부 정보를 잃을 수 있다는 것)이 문제가 됩니다.

물론 사용자의 의도일 수 있겠지만 일반적으로 data의 손실이 되도록 코드를 짜는 것은 좋지 않습니다.

class SecondType
{
    private MyType value;

    public static implicit operator MyType(SecondType t)
    {
        return t.value;
    }
}

class MainApp
{
    static void Main()
    {
        object o = Factory.GetObject();

        // o는 SecondType이라고 가정하면
        // 첫 번째 방법
        MyType t = o as MyType;

        if (t != null)
        {
            // MyType 타입의 t 객체 사용
        }

        // 두 번째 방법
        try
        {
            MyType t2;
            t2 = (MyType)o;

            // MyType 타입의 t2 객체 사용
        }
        catch (InvalidCastException e)
        {
            // 형변환 오류 보고
        }
    }
}

as 연산자와 캐스팅의 방법은 모두 실패합니다.

캐스팅 방법을 사용하면 사용자 정의 형변환 연산자가 사용된다고 했었으나,,

컴파일러는 런타임에 객체가 어떤 타입일지를 예측하지 못..(정적 타이핑 이므로)

컴파일러는 단순히 컴파일 타임에 객체가 어떤 타입으로 선언됐는지만 추적하기 때문에 모두 실패하게 됩니다.

 

컴파일러는 객체 o가 런타임에 어떤 타입인지를 알 방법이 없습니다.

컴파일러는 객체 o가 object 타입이라고 생각하고 object 타입을 MyType으로 형변환할 수 있는 연산자가 정의됐는지만을 확인합니다. 이러한 형변환 연산자를 정의하지 않았기 때문에 컴파일러는 이제 o 객체가 MyType 형식인지를 확인하는 코드만 생성합니다.

런타임에 o는 SecondType 형식의 객체이므로 형변환에 실패하게 됩니다.

컴파일러는 런타임에 o가 어떤 타입일 지를 예측하고 이를 기반으로 MyType으로 형변환이 가능한지를 고려하지는 않습니다.

 

사용자 정의 형변환 연산자는 객체의 런타임 타입이 아닌 컴파일타임 타입에 맞춰 수행된다는 점에 다시 한번 유의합니다.

런타임에 o 객체를 MyType으로 변환할 수 있는지는 중요하지 않고 이에 대해서 알지도 못하며 고려하지도 않습니다.

다만 사용자 정의 형변환 연산자가 정의되었다면?

어떤 타입으로 선언되었느냐에 따라 다르게 동작할 수 있습니다.

 

캐스팅보다 as 연산자를 사용한다라면 어떤 타입으로 선언되었든 간에 항상 동일한 결과를 반환해줍니다.

t = o as MyType;

설사 사용자 정의 형변환 연산자가 정의됐다 하더라도 o가 MyType이나 MyType을 상속한 타입이 아니라면 컴파일 오류를 발생합니다.

 

foreach

자주 사용하는 foreach 루프는 제너릭 타입이 아닌 IEnumerable 인터페이스를 사용합니다.

class MainApp
{
    // version 1
    public void UseCollection(IEnumerable theCollection)
    {
        foreach (MyType t in theCollection)
            t.DoSuff();
    }
    
    // version 2
    public void UseCollection2(IEnumerable theCollection)
    {
        IEnumerator it = theCollection.GetEnumerator();
        while (it.MoveNext())
        {
            MyType t = (MyType)it.Current;
            t.DoSuff();
        }
    }
}

foreach 문은 캐스팅을 통해서 루프에서 사용되는 타입으로 객체를 형변환합니다.

version 1이 흔히 사용하는 foreach 문이고, 아래 version 2는 위의 foreach 문이 생성하는 코드입니다.

 

foreach 문은 값 타입과 참조 타입 모두에 대해서 형변환을 지원해야 하는데 이때 캐스팅을 사용하면 대상 타입을 구분할 필요가 없어지게 됩니다. 하지만 이로 인해 InvalidCastException이 발생할 가능성이 존재합니다.

 

foreach 문장은 컬렉션 내부의 객체가 런타임에 어떤 타입인지, 그 타입을 루프 변수 타입으로 형변환 가능한지 등을 확인하지 않습니다. IEnumerator.Current의 반환형인 System.Object 타입으로 형변환 가능한지와, Object 타입의 객체를 다시 루프 타입 변수로 형변환 가능한지만 확인합니다.

 

특별히 객체의 타입이 정확히 MyType인 경우에만 동작하는 함수를 만들고자 한다면 더 정밀한 타입 비교가 필요한데,

.NET 기본 클래스 라이브러리에는 시퀀스 내의 개별 요소들을 특정 타입으로 형변환하는 Enumerable.Cast<T>() 메서드가 존재합니다.

class MainApp
{
    static void Main(string[] args)
    {
        IEnumerable collection = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        var small = from int item in collection
                    where item < 5
                    select item;

        var small2 = collection.Cast<int>().Where(item => item < 5).Select(n => n);
    }
}

여기서 Cast<T> 메서드는 as 연산자 대신 캐스트 연산을 수행하는데, as 연산자를 사용하면 형변환하려는 타입에 제한이 생기기 때문에 Cast<T> 메서드를 구현하는 대신 캐스트 연산을 사용하는 단일 Cast<T> 메서드만 작성했다고 합니다.

 

가능하면 형변환은 피하자.

사용자의 의도를 명확히 표현할 수 있는 is나 as 연산자를 사용하는 것이 좋습니다.

is나 as 연산자는 거의 예상대로 동작하며 대상 객체를 올바르게 형변환할 수 있을 경우에만 성공하고 나머지는 실패합니다.

 

캐스트 연산보다는 is나 as를 사용하는 것이 의도하지 않은 부작용이나 예상치 못한 문제를 피할 수 있는 좋은 방법이 됩니다.