nullable 형식 한정자




nullable 형식은 기본 값 형식의 모든 값과 추가로 NULL 값을 나타내는 형식입니다. 


Nullable<T> 또는 T? 의 형태로 사용할 수 있으며, struct와 임의의 값 형식이 될 수 있지만 참조 형식은 될 수 없습니다.


1
2
3
4
5
6
7
int a = null;
// 컴파일 에러
// int는 null을 허용하지 않는 형식.
 
int? b = null;
 
int result = b ?? 0;
cs


??(null 병합 연산자)나 ?.(null 조건부 연사자)와 같이 C#에서 null 값을 체크해주는 연산자들이 있습니다. 


이런 연산자를 사용하다 보면 null 값을 허용하지 않는 형식에도 사용이 필요한 경우가 있습니다.


이 때, nullable 형식 한정자를 사용하게 되면 null 값과 함께 내부 형식 값도 모두 나타낼 수 있습니다.



. null 병합 연산자 : https://loveme-do.tistory.com/8

. null 조건부 연산자 : https://loveme-do.tistory.com/7






참   고 : https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/nullable-types/using-nullable-types

?? 연산자 (null 병합 연산자)



 
?? 연산자는 피연산자가


1. NULL일 경우, 오른쪽 피연산자를 반환.

2. NULL이 아닐 경우, 왼쪽 피연산자를 반환.


1
2
3
int? a = null;
 
int cnt = a ?? 10;
cs


위와 같을 때,

anull 이라면, cnt = 10;

anull 이 아니라면, cnt = a; 의 값이 대입되게 됩니다. 


객체가 null인 경우에 대한 디폴트 값을 설정해 주거나, 이러한 상황을 안전하게 처리할 수 있습니다. 





참  고 : https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/operators/null-coalescing-operator

?. 연산자 (null 조건부 연산자)



 null 조건 연산자는 ? 앞에 있는 객체가 NULL인지 체크해서


1. NULL 이라면, NULL을 리턴.

2. NULL이 아니라면, ? 다음의 속성이나 메서드를 실행.


일반적으로 ?. 와 같이 표현되지만, 만약 인덱서 혹은 배열 요소에 적근할 경우는 ?[]와 같이 표현된다.


1
2
3
4
5
6
7
8
9

// customer 컬렉션이 null 이면 cnt는 null
// null이 아니라면, cnt는 customer의 갯수.
int? cnt = customer?.Count;
 
// customer가 null인지 체크하고
// 다시 customer[0]가 null인지 체크한다.
// customer이 null이거나 customer[0]이 null이라면 age는 null
// 그렇지 않다면, age는 customer[0].age
int? age = customer?[0]?.age;

cs



null 조건부 연산자를 사용하면 null 참조될 수 있는 객체에 안전하게 접근할 수 있으며,

null에 대한 예외 처리를 축약해서 개발자의 의도를 표현할 수 있다.






참  고: http://www.csharpstudy.com/CS6/CSharp-null-conditional-operator.aspx


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



[ Content ]

cast (캐스트)
    . 프로그래밍 언어에서, 객체의 유형을 다른 유형으로 바꾸는 것을 말한다.
    . 아래는 C형태의 캐스트로 '(타입)피연산자' 형태로 사용된다.


1
2
3
int i = 42;
char* p = &buf;
*p = (char)i;

cs



- as 연산자

    . 특정 형식의 변환을 수행한다. 캐스트 작업과 비슷하지만, 변환할 수 없는 경우 예외를 발생시키지 않고 null을 반환한다.
    . 참조 : https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/as


is 연산자

    . 지정된 형식으로 변환할 수 있는지, 런타임에 형식 호환성을 평가한다. 
      변환이 가능하면 true, 그렇지 않으면 false를 반환한다.
    . 참조 : https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/is


- is, as 연산자를 사용하는 것이 좋은 이유!

    1. 코드 작성이 용이하다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
object o = Factory.GetObject();
 
//----------- as 연산자를 사용한 버전
 
MyType t = o as MyType;
 
if(t != null)
{
    // MyType 타입의 t 객체 사용.
}
else
{
    // 오류 보고.
}
 
//----------- 캐스트를 사용한 버전
 
try
{
    MyType t;
    t = (MyType)o;
    
    // MyType 타입의 t 객체 사용.
}
catch(InvalidCastException)
{
    // 오류 보고
}
cs


  캐스팅을 사용한 경우에는 예외처리 코드와 null 확인 코드 모두 필요합니다.
반면, as를 사용하면 try / catch 문을 사용할 필요가 없습니다. 때문에 성능과 가독성이 좋고, null 확인 코드만 있으면 되죠!



    2. 더 안전하고 런타임에 효율적으로 작동한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SecondType
{
    private MyType _value;
 
    // 중략
    // 형변환 연산자 SecondType -> MyType
    public static implicit operator MyType(SecondType t)
    {
        return t._value;
    }
}
 
SecondType o;
// obejct o = Factory.Getobject();
// o는 SecondType
 
//----- as 연산자 사용.
 
MyType t = o as MyType; // o가 MyType이 아니면 실패.
 
if(t != null
{
    // MyType 으로 형변환된 t 사용.
}
else
{
    // 실패.
}
 
//----- 캐스트 사용.
 
try
{
    MyType t1;
    t1 = (MyType) o;
    
    //MyType 형변환 된 t1 사용.
}
catch(InvalidCastException)
{
    // 형변환 오류 보고.
}
cs


  위에 캐스트와 as 연산자를 이용한 예제가 있습니다. 결과를 먼저 말하자면 두 버전 모두 실패합니다..!!
캐스트로 형변환 하는 경우, 사용자 정의 형변환 연산자가 호출되어 캐스팅에 성공할 것 같지만 그렇지 않습니다.
그 이유는 컴파일러는 단순히 컴파일 타임에 객체가 어떤 타입으로 선언됐는지만 추적하기 때문입니다.
컴파일러는 객체 o가 런타임에 어떤 타입인지 알 수 없어 객체 o가 object 타입이라고 생각하고 object 타입을 MyType으로 형변환 할 수 있는 연산자가 정의되어 있는지를 확인합니다.


  as나 is는 런타임에 객체의 타입을 확인하지만, 필요에 따라 박싱을 수행하는 것을 제외하고는 어떠한 작업도 수행하지 않습니다. 따라서, as, is 형변환은 사용자 정의 형번은 수행되지 않습니다. as, is를 사용하여 형변환을 수행하려면 변환하고자 하는 객체는 지정한 타입이거나 지정한 타입을 상속한 타입이어야 합니다. 이 외에는 모두 실패하게 됩니다.


  다시 말해, t = st as MyType; 에서 st 가 MyType 이거나 MyType을 상속한 타입이 아니라면 컴파일 오류를 발생하게 될겁니다. 이처럼 as, is 연산자가 결과의 일관성이 높고 더 안전하며 런타임에 효율적입니다.


- 사용자 정의 형변환 : https://loveme-do.tistory.com/4?category=767223




[ Digression ]


  이미 C++을 공부하거나 C#을 공부하면서 C스타일의 형변환은 지양해 와서 as, is를 사용하는게 훨씬 익숙한 편인데, 캐스트가 컴파일 타임에 객체 타입을 확인해서 추적한다거나 사용자 정의 형변환은 이번에 알게 됐네요....!!
캐스트를 완전히 쓰진 않으니 써야할 경우 이에 좀 더 안전하게 대처할 수 있지 않을까 합니다.






참 고 : Effective C# 

C# 사용자 정의 형변환 - explicit, implicit



 두 기능 모두 C#4.0부터 지원해주고 있는 기능입니다.

explicit
 명시적 사용자 정의 형변화 연산자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Celsius
{
    private float degress;
 
public Celsius(float degress)
    {
        this.degress = degress;
    }
    //Celsius -> Fahrenheit 명시적 형변환.
    public static explicit operator Fahrenheit(Celsius c)
    {
        return new Fahrenheit((9.0f / 5.0f) * c.degress + 32);
    }
    public float Degress
    {
        get { return this.degress; }
    }
}
 
class Fahrenheit
{
    private float degress;
 
    public Fahrenheit(float degress)
    {
        this.degress = degress;
    }
    //Fahrenheit -> Celsius 명시적 형변환.
    public static explicit operator Celsius(Fahrenheit fahr)
    {
        return new Celsius((5.0f / 9.0f) * (fahr.degress - 32));
    }
    public float Degress
    {
        get { return degress; }
    }
}
 
class TextExplicit
{
    static void Main()
    {
        Fahrenheit fahr = new Fahrenheit(100.0f);
        Celsius c = (Celsius)fahr; // Fahrenheit -> Celsius explicit conversion.
 
        Fahrenheit fahr2 = (Fahrenheit)c; //Celsius -> Fahrenheit explicit conversion.
    }
}



- implicit
 암시적 사용자 정의 형변화 연산자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Digit
{
    byte value;
 
    public Digit(byte value)
    {
        if (value > 9)
        {
            throw new System.ArgumentException();
        }
        this.value = value;
    }
    // byte -> Digit 암시적 형변환.
    public static implicit operator Digit(byte b)
    { 
        return new Digit(b);
    }
    // Digit -> byte 암시적 형변환.
    public static implicit operator byte(Digit d)
    {
        return d.value;
    }
}
 
class TextImplicit
{
    static void Main()
    {
        Digit d = new Digit(3);
        byte b = d; // Digit -> byte implicit conversion.
 
        Digit dig2 = b; // byte -> Digit implicit conversion.
    }
}






참 고 : https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/statements-expressions-operators/using-conversion-operators

const보다는 readonly가 좋다.



[ Content ]

- const
    . 컴파일 타임 상수.
    . 컴파일 타임에 변수가 값으로 대체되어, 메서드 내부에서도 선언이 가능.
    . 컴파일 타임에 상수를 리터럴로 대체해야 하기 때문에, 내장된 숫자형, enum, 문자열, null과 같은 내장 자료형만 사용 가능.

- readonly
    . 런 타임 상수.
    . 컴파일 타임에 값으로 대체되는 것이 아닌, 상수에 대한 '참조'로 컴파일되어 메서드 내에서는 선언 불가능.
    . 꼭 생성과 동시에 초기화하지 않아도 되며, 생성자에서 최초 1번 초기화 가능. 그 이후에는 수정 불가능.
    . 어떤 타입과도 사용될 수 있다.


 위 내용의 런타임 상수와 컴파일 상수의 차이 중, 중요한 차이는 값이 런타임에 평가된다는 점입니다. 
런타임 상수를 참조하는 코드를 컴파일하면 컴파일 타임 상수처럼 코드를 값으로 대체하지 않고, 참조 코드를 생성합니다.
이러한 차이가 유지 및 보수에 상당한 영향을 미칩니다.


1
2
3
4
5
public class UsefulValues
{
    public static readonly int StartValue = 5;
    public const int EndValue = 10;
}



 위와 같이 정의하여 값을 사용하다 아래와 같이 수정할 때,


1
2
3
4
5
public class UsefulValues
{
    public static readonly int StartValue = 105;
    public const int EndValue = 110;
}



 수정 후, 리빌드하지 않고 사용한다면, StartValue에 대한 값은 참조 코드로 인해 런타임에 105로 읽어드리지만, EndValue는 이전 컴파일 시점에 대체되었던 10으로 값이 유지됩니다.

 이처럼 컴파일 타임 상수의 값을 수정할 때는 신중해야 하며, 모든 코드를 재컴파일해야 합니다. 하지만, 런타임 상수는 값 변경만으로도 수정이 가능하며 이진 호환성도 그대로 유지됩니다.


 이와 반대로 재컴파일 전까지 기존 값을 유지하고 싶은 경우도 있을겁니다. 이러한 경우 컴파일 타임 상수를 활용하면 됩니다.
또한, 선택적 매개 변수에 대한 기본값, 특성의 매개 변수, switch/case문의 레이블, enum 정의에 사용되는 상수 등도 컴파일 시에 사용되어야 하므로 const를 통해 초기화 해야합니다.
        - 선택적 매개 변수 : https://loveme-do.tistory.com/2

 const가 가지는 장점은 성능이 빠르다는 것인데, 이를 통해 얻을 수 있는 성능 개선 효과가 크지 않아, 위와 같은 예외적인 상황을 제외한다면 readonly를 사용하는 것이 좋습니다.




[ Digression ]


 아직 많지 않은 경험으로 사용하면서 가장 크게 느낀 장점은 readonly가 모든 타입과 사용할 수 있다는 점입니다.
상수 사용에 내장 타입과도 많이 사용하지만 직접 정의한 타입을 사용하는 경우도 많아 자주 사용할 수 밖에 없습니다. 
 위의 내용처럼 유지 및 보수 시에 확실히 상수 값 변경에 부담을 가지지 않아도 된다는 점은 크게 느낄 수 있는 장점이라 생각합니다.






참 고 : Effective C# 

C# 선택적 매개 변수와 명명된 매개 변수.



 두 기능 모두 C#4.0부터 지원해주고 있는 기능입니다.

-
선택적 매개변수
 C++
에서의 디폴트 매개 변수와 같은 기능으로, 매개 변수를 필수 또는 선택 사항으로 지정할 수 있습니다.
호출 시 모든 필수 매개 변수에 대한 인수를 제공해야 하지만 선택적 매개 변수에 대한 인수는 생략할 수 있습니다.
이 때, 선택적 매개 변수에 대해서는 기본값을 필수적으로 제공해주고 있어야하며, 필수 매개 변수 다음으로 정의해야 합니다.

 선택적 매개 변수 지원 이전에는 다음과 같이 필요한 매개 변수에 따른 함수 오버로딩으로 정의해야 했는데.....


void Person(string name);

void Person(string name, int age);
void Person(string name, int age, string address);


 위의 코드를 선택적 매개 변수 기능으로 아래와 같이 수정 가능해졌습니다!

 

void Person(string name, int age = 0, string address = “None”);



-
명명된 매개 변수
호출된 메서드, 각 인수에 대한 매개 변수를 매개 변수 이름으로 지정할 수 있습니다.


 

Person(name: “승엽, address: 서울특별시, age: 25);


 위와 같이 각 인수에 대한 매개 변수를 매개 변수 이름으로 지정하여 사용할 수 있습니다.
각 인수가
 무엇을 나타내는지 식별할 수 있어 코드의 가독성을 향상시킬 수 있고, 임의의 순서로 인수를 보낼 수 있습니다.

 또한, 위의 Person의 선택적 매개 변수 중 address에만 인자를 보내고 싶을 경우에 다음과 같은 처리도 가능합니다.

 

Person(“민호, address: 경기도);








참 고 : https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/classes-and-structs/named-and-optional-arguments

지역 변수를 선언할 때는 var를 사용하는 것이 낫다.



[ Content ]
- var : 익명 타입을 제공하는 C#에서 타입을 암시적으로 선언하는 키워드.


- 이유 
    1. 적절한 사용으로 코드의 가독성을 높일 수 있다.


 예를 들어, Dictionary<int, Queue<String>>과 같은 기술 타입 자체보다 타입을 유추할 수 있는 변수 이름과 암시적 타입 선언을 사용하는 것이 가독성을 올릴 수 있습니다.

 당연하지만, 무분별한 사용은 오히려 가독성을 해칠 수 있죠!! 
특히, 내장 숫자 타입들과 var를 함께 사용할 때는 주의해야 합니다. 내장 숫자 타입들은 다양한 형변화 기능을 가지고 있고, 정밀도도 각기 다르기 때문에 숫자 타입과 var를 함께 사용하면 가독성과 정밀도에 관련되어 혼돈을 유발할 가능성이 있기 때문입니다. 또한 변수 타입이 쉽사리 짐작되지 않는 경우라면, 암시적 선언보다는 명시적을 선언하는 편이 낫습니다.


2. 쿼리문 사용 시, 반환 타입이 IEnumerable<T>와 IQueryable<T> 중, 정확한 타입을 알지 못한 채, 명시적으로 타입을 지정하게 된다면, 득보다 실이 많을 수 있다. 


 IQueryable<T>를 IEnumerable<T>로 받게되면 강제 형변환이 발생하고, IQueryProvider가 제공하는 장점을 읽게 됩니다. 이 때, IQueryable<T>는 IEnumeable<T>를 상속받은 것으로 형변환이 일어날 뿐, 에러가 발생하지 않아 파악하는데 어려움이 생깁니다.

 . IEnumerable<T>와 IQueryable<T> 차이
   http://monkeychoi.blog.me/220556573557



[ Digression ]


 실제로 코드를 작성하면서 var타입을 많이 사용하고 있습니다. 프로젝트 상의 상속구조와 객체들이 많아지면 함수의 이름이나 변수의 이름으로 필요한 것을 찾아 쓰고 이에 따른 반환 값이나 지역 변수를 사용할 때 var타입을 사용하는 편입니다. 생각보다 복잡하게 선언된 타입들도 많아 코드량도 줄일 수 있고 가독성을 높일 수 있다고 생각합니다.

 초반 작업에는 내장 숫자 타입을 var로 받아 의도치 않은 정밀도를 봤던 경우도 있었습니다. 위의 내용처럼 무분별한 사용이 아닌 적절히 사용한다면 확실히 편하게 사용할 수 있는 부분들이 많습니다.






참 고 : Effective C# 

+ Recent posts