C#은 데이터를 쉽고 효율적으로 처리하기 위해 LINQ라는 것을 지원해준다.

SQL 문법을 사용해서 데이터를 간편하게 탐색하고 처리하는 것이다.

쉽게 생각하면 데이터베이스를 다루듯이 C#의 자료구조를 다룬다고 생각하면 된다.

 

먼저, 아래 한 가지 상황을 가정해보자.

int[] Arr = { 1, 7, 3, 2, 8 };

위와 같은 배열에서 5 이상의 원소만 검출하고 싶다고 해보자.

가장 단순하게 우리가 생각할 수 있는 건 아래와 같은 방식일 것이다.

List<int> SelectedNum = new List<int>();

for(int Index = 0; Index < Arr.Length; Index++ )
{
    if (Arr[Index] >= 5)
    {
        SelectedNum.Add(Arr[Index]);
    }
}

for문을 통해 배열은 순회하면서 5 이상의 원소를 모두 걸러내는 것이다.

아주 간단하다.

 

그렇다면, 이번엔 아래 상황을 가정해보자.

struct Student
{
    public Student(string name, int age, int score)
    {
        _name = name;
        _age = age;
        _score = score;
    }

    public string _name; //이름
    public int _age; // 나이
    public int _score; // 성적
}

namespace Test
{
    public class MainClass
    {
        static int Main()
        {
            Student[] Arr = { new Student( "김민수", 13, 80 ), 
                              new Student("최종훈", 18, 90), 
                              new  Student("박상민", 14, 75)};

            return 0;
        }
    }
}

 

이름, 나이, 성적을 필드로 가진 Student 구조체가 있고, 위의 코드처럼 3명의 학생이 있다고 해보자.

여기서, 점수가 80점 이상인 학생의 나이만 검출해서 그 평균값을 구하고 싶다고 해보자.

 

예를 들어, 위의 세 학생중에서 점수가 80점 이상인 학생은 김민수, 최종훈이고 그 둘의 나이는 13살, 18살이다.

두 나이의 평균값은 15.5이므로, 구하고자 하는 값은 15.5가 될 것이다.

 

이를 구하는 가장 단순한 방법은 아래와 같을 것이다.

 Student[] Arr = { new Student( "김민수", 13, 80 ), new Student("최종훈", 18, 90), new  Student("박상민", 14, 75)};

 int AgeSum = 0;
 int NumSelected = 0;

 foreach (Student student in Arr)
 {
     if(student._score >= 80)
     {
         AgeSum += student._age;
         NumSelected++;
     }
 }

 float Answer = (float)AgeSum / NumSelected;

 return 0;

Arr을 순회하면서, 점수가 80점 이상인 친구들의 나이를 AgeSum 이라는 지역변수에 모두 더한 뒤, 점수가 80점 이상인 학생들의 수로 AgeSum을 나누어 평균을 구하는 것이다.

 

첫 번째 예시에 비해, 조건이 다양해지니 조금 복잡해 진 것을 알 수 있다.

 

만약 여기보다 조건이 더 많아진다고 해보자.

예를 들어, 성별, 나이, 재산, 이름, 거주지 등 여러 필드를 가진 구조체를 원소로 보유한 자료구조에서

13살 이상인 여성의 재산 평균을 구한다든가, 경기도에 사는 20세 미만 남성의 재산 총 합을 구한다든가 이런식으로 조건이 많아질수록 식은 점점 복잡해질 것이다.

 

그렇다면, LINQ를 사용하면 어떨까?

위의 두 번째 예시를 LINQ로 한 번 작성해보겠다.

Student[] Arr = { new Student( "김민수", 13, 80 ), new Student("최종훈", 18, 90), new  Student("박상민", 14, 75)};

var Selected = from student in Arr
               where student._score >= 80
               select student._age;

float Answer = (float)Selected.Sum() / Selected.Count();

return 0;

아주 간단해졌다. 사실 위의 예제도 크게 복잡한 것은 아니라서 체감이 크게 안될 수도 있지만, LINQ를 사용하면 데이터를 단순하고 쉽게 처리할 수 있게 된다.

 

중요한 것은 아래의 구문이다.

var Selected = from student in Arr
               where student._score >= 80
               select student._age;

from이란, 자료구조의 원소를 받을 멤버 변수를 선언하는 것이다.

where이란, 자료구조의 원소 중에서 어떤 조건을 만족하는 대상을 식별할 것인지를 명시하는 것이다.

select란, 명시된 대상 중 어떤 값을 검출할 것인지를 정하는 것이다.

 

더 자세히 알아보자.

from student in Arr

Arr의 원소를 student라는 대상에 넣어서 읽겠다는 의미이다.

foreach(Student A in Arr)
{
}

위의 foreach 문과 의미가 같다.

 

where student._score >= 80

이건, _score가 80이 이상인 대상만 식별하겠다는 의미이다.

foreach(Student A in Arr)
{
    if(A._score >= 80)
    {
    }
}

위의 foreach문 내부의 if문과 의미가 같다.

 

select student._age

이는, where문을 만족한 student 중에서 그 _age를 저장하겠다는 의미이다.

List<int> Selected = new List<Student>();

foreach(Student A in Arr)
{
    if(A._score >= 80)
    {
        Selected.Add(A._age);
    }
}

위의 foreach문 내부의 if문 내부에 있는 Add구문과 의미가 동일하다.

 

var Selected = from student in Arr
               where student._score >= 80
               select student._age;

즉, 위의 구문을 정리해보자면 Arr 내부에 있는 원소중 성적이 80 이상인 학생들의 나이를 Selected에 저장하겠다는 의미이다.

float Answer = (float)Selected.Sum() / Selected.Count();

이후, Selected의 Sum 메서드를 통해 멤버함수의 합을 구한 뒤, Selected의 원소 개수로 나누면 구하고자 했던 값을 구할 수 있게 된다.

 

이 외에도, LINQ의 쿼리문은 검출된 대상을 정렬할 수도 있다. 일반적으로 LINQ의 쿼리문은 첫 번째 원소부터 순회하며 데이터를 판별하기 때문에, Selected에는 조건을 만족하는 원소 중 앞에 있는 원소부터 순차적으로 저장될 것이다. 

 

var Selected = from student in Arr
               where student._score >= 80
               orderby student._name
               select student._age;

위의 구문을 보면 아까는 없던 orderby라는 쿼리문이 추가되었다.

이는 정렬 기준을 선택하는 것이다. 위의 구문에선 student._name을 orderby 뒤에 명시했기 때문에, 저장되는 _age는 _name이 사전순으로 앞에 오는 student의 _age가 먼저 저장되도록 정렬될 것이다. 

 

만약, 저장되는 나이를 그대로 오름차순으로 저장하고 싶다면, orderby 뒤에 student._age를 명시하면 된다.

또한, 내림차순으로 정렬하고 싶다면, orderby (정렬기준) 뒤에 descending 을 붙여주면 된다.

var Selected = from student in Arr
               where student._score >= 80
               orderby student._name descending
               select student._age;

위와 같이 descending을 붙이면 내림차순으로 정렬이 된다.

즉 위의 구문을 해석해보면 이와 같다.

 

Arr에 있는 원소중, _score이 80인 대상을 검출한 뒤, 검출된 대상들을 _name을 기준으로 내림차순 정렬하고, 정렬된 상태에서 _age를 순차적으로 검출하겠다는 것이다.

 

이는 매우 기본적인 LINQ 문법이며, 실제로는 이보다 더 다양한 문법이 제공된다.

쿼리문의 종류도 더 있고, 람다함수를 활용해 메서드로 질의하는 방법도 있다.

이는 다음 게시물에서 알아보도록 하자.

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

C# - ref, out  (0) 2024.08.13
C# - const, readonly  (0) 2024.08.10
C# - is, as 연산자  (0) 2024.08.07
C# - 클래스, 접근 제한 지정자, 프로퍼티  (0) 2024.07.27
C# - C#의 자료형  (0) 2024.07.24

C#에는 값을 참조형으로 사용하기 위한 두가지의 키워드를 제공해준다.

바로, ref와 out이다.

더보기

참조 전달이란, 값을 그대로 전달하는 것이 아니라 값이 저장된 메모리 영역이 어디인지를 전달하는 것이다.

 

값을 그대로 전달하게 되면, 함수 내부에서 그 값을 사용하기 위해 복사가 발생하게 된다. 하지만, 메모리 영역의 위만 전달하게 된다면 함수 내부에선 값을 복사하지 않고도 위치를 참조해 변수에 직접 접근할 수 있게 된다.

 

이해가 안된다면, call by value와 call by reference에 대해 검색해보자.

두 키워드는 무슨 차이가 있을까? 사실 근본적으로는 거의 비슷하다. 역할도 기능도 방식도 그렇다.

하지만, 차이점은 몇 가지 존재한다.

 

1. ref 키워드가 사용된 변수는 가리키는 대상이 초기화가 되어있어야 하지만, out은 초기화가 되어 있지 않아도 된다.

void Func()
{
    int A = 3;
    int B;
    
    //오류X
    Add_5(ref A);
    
    //오류 발생
    Add_5(ref B); 
}

void Add_5(ref int input)
{
    input += 5;
}

Add_5 함수에 값을 참조전달하기 위해 ref키워드를 사용하게 되면 B의 경우엔 오류가 발생한다.

이유는 대상이 초기화가 되어 있지 않기 때문이다.

하지만, Add_5의 파라미터를 out int 로 변경하여 사용한다면 또 다른 에러가 발생할 것이다. 이유는 아래 조건 때문이다.

 

2. out 키워드가 사용된 변수는 함수 내부에서 사용되기 전에 초기화가 반드시 이루어져야 한다.

void Func()
{
    int A = 3;
    int B;

    //오류X
    Set_5(out A);

    //오류X
    Set_5(out B);
}

void Add_5(out int input)
{
    //오류 발생
    input += 5;
}

void Set_5(out int input)
{
    input = 5;
}

위의 함수를 보면, Add_5함수는 값에 5를 더해주는 형식으로 사용하고 있다.

그런데, 사용할 변수의 값이 초기화가 안되어있기 때문에 값을 더하는 연산은 의도하지 않은 결과를 일으킬 수 있다.

이러한 위험성을 제거하기 위해 out키워드를 사용해 전달된 값은 반드시 내부에서 초기화를 해주어야 한다.

 

Set_5 함수의 경우 내부에서 값을 초기화해주고 있기 때문에, 에러가 발생하지 않는다.

이처럼 out키워드를 사용하여 참조 전달을 할 때엔, 대상을 초기화하지 않은 상태로 전달해도 되지만 그 내부에서는 우선적으로 초기화가 이루어져야만 한다.

 

3. out 키워드는 함수의 파라미터에만 사용이 가능하지만, ref는 그 외에도 사용할 수 있다.

void Func()
{
    //오류X
    int A = 3;
    ref int B = ref A;

    //오류 발생
    int C = 5;
    out int D = out C; 
}

이런걸 쓸 일이야 거의 없겠지만, 위처럼 ref는 같은 스코프 내에서 변수를 선언해서 사용할 수도 있다.

또한, C# 버전에 따라 멤버 변수에도 사용할 수가 있다.

 

반면, out키워드는 위와 같은 사용이 불가능하다.

 

ref와 out을 구분하는 이유

위의 차이를 생각해보면 사실 out키워드를 사용할 필요는 딱히 없어보인다.

ref 키워드로 out 키워드를 완전히 대체할 수 있기 때문이다.

 

그럼에도 불구하고 out 키워드가 존재하는 이유는 뭘까?

바로 그 의미를 확실하게 하기 위함이다.

 

참조 전달(call by reference)를 사용하는 이유는 크게 2가지가 있다.

 

1. 외부 변수의 값을 사용하기 위해 (읽기)

2. 외부 변수의 값을 변경하기 위해 (쓰기)

 

1번의 경우 간단하다. 외부에 있는 어떠한 변수의 값을 함수 내부에서 사용하고 싶은데 값형으로 전달하게 되면 복사가 발생하기 때문에 참조형을 통해 불필요한 복사를 막기 위해 참조 전달을 사용하는 것이다.

 

예를 들어 원소가 10만개인 배열을 사용할 때, 값형으로 전달하게 되면 10만번의 복사가 발생하지만, 참조전달을 하게 되면 이러한 복사가 발생하지 않기 떄문이다.

 

즉 성능 측면에서 큰 이득이 있기 때문이다.

 

2번의 경우는 어떨까? 이 역시도 1번과 동일하게 복사를 줄이고 성능을 향상시키기 위함이 크다.

 

만약 함수 내부에서 연산 결과 값을 배열에 저장하여 10만개의 원소를 보유하게 되었다고 했을 때, 이 배열을 값형으로 반환하게 되면 저장하는 과정에서 복사가 발생하게 될 것이다. 반면, 빈 배열을 참조형으로 전달한 뒤에 함수 내부에서 원소를 삽입하게 되면 불필요한 복사가 발생하지 않게 될 것이다.

 

결국 참조 전달은 성능의 향상을 위해 사용하는 것이 가장 큰 목적인 것이다.

그런데 위에서 말했듯이, ref 키워드만 있어도 두 이유를 모두 만족시킬 수 있다.

 

하지만, 코드를 좀 쳐본 사람이라면 알겠지만 프로그래밍에 있어서 가독성은 아주아주 중요한 역할을 한다. 이 변수가 어떻게 사용될 지 함수의 선언만 보고도 어느정도 유추할 수 있게 설계하는 것은 아주 중요한 일이다. out 키워드는 이를 확실히 하기 위해 사용된다.

 

함수의 파라미터가 out으로 선언되어 있다면, 우리는 그 키워드만 보고서 "아, 변수를 넣어주면 함수 내부에서 그 안에 값을 넣어주겠구나" 라는 예측이 가능하다는 것이다.

 

ref키워드만 모두 사용하게 된다면, 이를 구분할 수가 없을 것이다.

 

즉, 변수가 어떻게 사용되는지 그 의미를 확실하게 알려주기 위해 사용되는 키워드인 것이다.

하지만, out 키워드를 사용했는데 내부에서 값을 저장해주지 않는다면? 

 

외부에서 그 함수를 호출해 변수를 전달한 사람은 변수에 올바른 값이 저장되어 있을 것이라고 생각했겠지만, 당연히 알 수 없는 값이 들어있을 것이고 이는 의도치 않은 오류를 야기시킬 것이다.

 

즉, 가독성을 높이려다가 안정성을 엄청나게 해치는 꼴이 되어버릴 수 있다는 것이다. 이러한 실수를 방지하기 위해, out키워드를 사용해 전달한 변수는 함수 내부에서 반드시 초기화가 이루어지도록 강제되고 있다. 언어 차원에서 프로그래머의 실수를 막아주는 것이다.

 

즉, out은 함수 내부에서 값을 저장해주는 상황에 사용하는 키워드이며 ref는 내부에서 값을 사용할 때 사용카는 키워드라고 생각하면 좋다. (물론 ref를 사용해도 내부에서 값을 저장할 수 있다. 하지만, out과 그 역할을 구분하여 사용하는 편이 좋다.)

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

C# - LINQ (Language Integrated Query)  (0) 2024.09.02
C# - const, readonly  (0) 2024.08.10
C# - is, as 연산자  (0) 2024.08.07
C# - 클래스, 접근 제한 지정자, 프로퍼티  (0) 2024.07.27
C# - C#의 자료형  (0) 2024.07.24

C#에는 값을 상수화하는 방법으로 2개의 키워드를 제공해준다.

바로 const와 readonly이다.

(상수화 : 변수에 저장되어 있는 값을 바꿀 수 없도록 하는 것)

 

그렇다면, 두 키워드에는 무슨 차이가 있을까?

가장 큰 차이는 const로 선언된 변수는 컴파일 타임에 값이 결정되고, readonly는 런타임에 결정된다는 것이다.

 

const 변수는 컴파일 타임에 값이 정해져야 하기 때문에 선언을 할 때 반드시 값을 초기화해야 한다.

//1
const int A;
A = 3;

//2
const int A = 3;

A라는 변수에 3이라는 값을 할당하는 방법은 위처럼 2가지가 있지만, const 변수에게 있어 1번 방식은 허용되지 않는다.

왜냐하면 컴파일 타임에 값을 정할 수 없기 때문이다.

 

이러한 이유로 const가 붙은 필드(멤버 변수)를 보유한 클래스의 경우, 모든 인스턴스가 같은 값을 공유하게 된다.

그렇다면 생각해보자. 모든 인스턴스가 같은 값을 공유할 것이 확실하다면, 굳이 인스턴스별로 메모리를 계속 할당할 필요가 있을까? 당연히 그럴 필요가 없다.

 

그래서 const 변수는 static이랑 동일하게 작동하게 된다. 클래스 단위로 1개만 생성되며, 데이터 영역에 위치하게 된다.

 

const가 컴파일 타임에 값을 결정해야 한다는 이유로 발생하는 특징이 하나 더 있다.

바로 커스텀 클래스에는 const를 사용할 수 없다는 것이다. 

왜냐하면, 클래스의 경우 생성자가 호출되어야만 그 값이 확정되기 때문에 컴파일 타임에는 정확한 값을 결정할 수가 없기 때문이다.

 

이번엔 readonly에 대해 알아보자.

 

readonly는 const와 다르게 생성자에서 한 번 초기화하는 것이 가능하다. 생성자가 아닌 곳에서는 불가능하다.

이러한 특성 때문에, 생성자 파라미터를 활용하면 인스턴스 별로 다른 값을 가지게 하는 것이 가능하다.

 

하지만, 생성자에서만 초기화 된다는 이유 때문에 한 가지 문제가 생긴다.

예를 들어, 아래 코드를 보자.

public void Function()
{
    readonly int A;
    A = 3;
}

우리는 상수화된 값을 지역 내에서 선언하고 사용하고 싶을 수도 있다.

그런데 readonly는 생성자에서만 값을 초기화하는 것이 가능하다고 했다.

 

그럼 위의 코드는 작동할까? 당연히 작동하지 않는다.

readonly는 이러한 이유로 지역변수로 사용할 수 없고 필드(멤버 변수)로만 사용이 가능하다.

 

readonly는 위에서 언급했던 것처럼 생성자에서 초기화되기 때문에 각 인스턴스가 다른 값을 가질 수도 있다.

이런 이유로 const처럼 static으로 선언되지 않고 일반 변수처럼 선언된다.

인스턴스가 100개라면 100개의 변수가 생성되는 것이다.

 

그렇기 때문에 const보단 더 유연하게 사용할 수 있겠지만, 메모리 사용량은 더 크다는 단점이 생긴다.

또한, readonly의 경우 힙영역에 생성되기 때문에 const변수보다 읽기 연산이 더 느릴 수 있다. 

 

하지만, readonly의 경우 const와 다르게 커스텀 클래스에도 사용이 가능하기 때문에 const보다는 더욱 광범위하게 사용이 가능하다.

 

두 키워드의 차이를 표로 한 눈에 알아보자.

const readonly
컴파일 타임에 값이 결정되어야 한다. 런타임에 값이 결정되어도 된다.
선언과 초기화가 함께 이루어져야 한다. 선언과 초기화를 분리할 수 있다. (초기화는 생성자에서 가능)
커스텀 클래스에는 사용이 불가능하다. 모든 자료형에 대해 사용이 가능하다.
static변수와 같이 프로세스 전체에 1개만 생성된다. 인스턴스의 개수만큼 변수가 생성된다.
데이터 영역에 위치한다. 힙 영역에 위치한다.
필드와 지역 변수에 모두 사용할 수 있다. 필드에만 사용이 가능하다.

 

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

C# - LINQ (Language Integrated Query)  (0) 2024.09.02
C# - ref, out  (0) 2024.08.13
C# - is, as 연산자  (0) 2024.08.07
C# - 클래스, 접근 제한 지정자, 프로퍼티  (0) 2024.07.27
C# - C#의 자료형  (0) 2024.07.24

코드를 작성하다 보면, 상속 관계를 상당히 많이 사용하게 된다.

is와 as는 두 클래스간의 상속 관계를 알려줌으로써 더욱 안전하게 상속 관계를 사용할 수 있게 해주는 키워드이다.

 

먼저, is연산자는 인스턴스가 특정 클래스로 캐스팅이 될 수 있는가를 true, false로 알려주는 키워드이다.

using System;
using Test;

namespace Test
{
    public class A
    {

    }

    public class B : A
    {

    }

    public class C
    {

    }
}

class MainClass
{
    public static void Main()
    {
        A newA = new A();
        B newB = new B();
        C newC = new C();

        bool AisB = newA is B;
        bool BisA = newB is A;

        bool AisC = newA is C;
        bool BisC = newB is C;
        bool CisA = newC is A;
        bool CisB = newC is B;

        Console.WriteLine(AisA);
        Console.WriteLine(AisB);
        Console.WriteLine(BisA);
        Console.WriteLine(BisB);

        Console.WriteLine();

        Console.WriteLine(AisC);
        Console.WriteLine(BisC);
        Console.WriteLine(CisA);
        Console.WriteLine(CisB);
        
        return 0;
    }
}

 

위의 코드를 보자.

 

B는 A를 상속받고 있기 때문에, 당연히 newB는 A로 업캐스팅이 가능하다.

하지만, A는 B의 부모클래스이기 때문에 처음부터 A로 생성된 인스턴스는 B로 다운캐스팅이 불가능하다.

C는 A,B와 어떠한 상속관계도 아니므로 당연히 서로 캐스팅이 불가능하다.

 

위의 결과를 보면 아래와 같다.

 

B는 A로 업캐스팅이 가능하기 때문에 newB is A 만이 true로 표시된 것을 볼 수 있다.

이처럼 인스턴스가 특정 클래스로 캐스팅이 가능한지 여부를 반환해주는 것이 is 연산자이다.

 

as연산자는 is연산자의 기능과 더불어 실제 캐스팅까지 수행해주는 연산자이다. 아래 코드를 보자.

using System;
using Test;

namespace Test
{
    public class A
    {

    }

    public class B : A
    {

    }
}

class MainClass
{
    public static void Main()
    {
        A newA = new A();
        B newB = new B();

        B CastedB = newA as B;
        A CastedA = newB as A;

        if(CastedB == null)
        {
            Console.WriteLine("CastedB is NULL");
            Console.WriteLine();
        }
        else
        {
            Console.WriteLine("CastedB is not NULL");
            Console.WriteLine();
        }

        if (CastedA == null)
        {
            Console.WriteLine("CastedA is NULL");
            Console.WriteLine();
        }
        else
        {
            Console.WriteLine("CastedA is not NULL");
            Console.WriteLine();
        }
    }
}

 

실행해보면 아래와 같다.

CastedB는 null을 가리키고 있으며, castedA는 null이 아닌 대상을 가리키고 있다.

 

즉, 캐스팅이 가능하다면 캐스팅된 대상을 반환해주며, 캐스팅이 안된다면 null을 반환하는 것이다.

is연산자처럼 캐스팅 여부를 파악할 수 있을 뿐더러 실제 캐스팅까지 실행해주는 연산자인 것이다.

 

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

C# - ref, out  (0) 2024.08.13
C# - const, readonly  (0) 2024.08.10
C# - 클래스, 접근 제한 지정자, 프로퍼티  (0) 2024.07.27
C# - C#의 자료형  (0) 2024.07.24
C# - 클래스와 콘솔 출력  (2) 2024.07.24

C#은 클래스를 어떻게 정의하는지 알아보자.

C++의 클래스와 거의 동일하다. 다만 조금 다른 부분이 있다.

 

C++은 아래와 같이 클래스를 정의한다.

class A
{
public:
    void func(){}
}

 

C#에선 아래와 같이 정의한다.

class A
{
    public func() {}
}

 

C++에선 접근 제한 지정자를 작성하면 그 아래에 있는 코드들은 해당 접근 제한 지정자의 영향을 받는다.

반면, C#은 접근 제한 지정자를 변수, 메소드 별로 앞에 붙여주어야 한다.

 

접근 제한 지정자는 C++에는 public, protected, private 3가지가 있고, C#에도 동일하게 있다. 역할 또한 똑같다. 

다만, C#에는 internal과 protected internal 이라는 2개의 접근 제한 지정자가 추가로 있다. 

 

internal은 같은 어셈블리 내에서만 접근이 가능하도록 제한하는 키워드이다. 그렇다면 어셈블리란 무엇일까?

컴파일을 통해 나온 결과물을 어셈블리라고 한다. exe파일도 하나의 어셈블리고, dll파일도 하나의 어셈블리이다.

 

어셈블리 내에서만 접근할 수 있도록 제한한다는 의미는 해당 프로젝트가 라이브러리로 컴파일되어 다른 프로젝트에서 사용되었을 때, 외부에서는 접근을 하지 못하도록 막는다는 의미인 듯 하다.

 

표로 정리해보면 아래와 같다.

접근 제한 지정자 C++ C#
public 어디에서나 참조 가능 어디에서나 참조 가능
protected 상속 관계에서만 참조 가능 상속 관계에서만 참조 가능
private 클래스 내부에서만 참조 가능 클래스 내부에서만 참조 가능
internal 없음 어셈블리 내에서만 참조 가능
protected internal 없음 어셈블리 내에 있는 상속 관계의 클래스에서만 참조 가능

 

C#에는 C++과 달리 프로퍼티라는 기능도 지원해준다.

프로퍼티란 쉽게 말하면 getter, setter이다.

 

C++에선 멤버 변수를 외부에서 안전하게 참조할 수 있도록 getter와 setter을 아래와 같이 만든다.

class Test
{
public:
    int GetA()
    {
        return A;
    }
    
    void SetA(int _A)
    {
        A = _A;
    }
    
private:
    int A = 0;
}

 

물론 C#도 위와 같이 getter, setter을 선언하여 사용할 수도 있다.

하지만, C#에서 지원해주는 프로퍼티란 기능을 사용할 수도 있다.

 

아래 코드를 보자.

class Class1
{
    public int AProperty
    {
        get 
        { 
            return A; 
        }

        set 
        {
            A = value; 
        }
    }

    private int A = 0;

}

 위와 같이, get set 키워드를 통해 getter과 setter을 간단하게 선언할 수 있다.

class Class1
{
    public int AProperty
    {
        get 
        { 
            return A; 
        }

        set 
        {
            if(A > 0)
            {
                A = value; 
            }
        }
    }

    private int A = 0;

}

이렇게 내부에 조건문을 추가할 수도 있고 선행작업이나 후행작업이 필요하다면 그 것도 추가할 수 있다.

 

class Class1
{
    public int AProperty
    {
        get { return A; }
        set {if(A > 0) A = value;}
    }

    private int A = 0;

}

이렇게 짧게 쓸 수도 있고, get이나 set중에 하나만 넣어줄 수도 있다.

 

일반적인 getter, setter이랑 사실 크게 다른 건 없어보인다. 그래도 문법에서 지원해주니까 프로퍼티를 사용하는게 아무래도 코드의 통일성을 유지하기 좋을 것 같다. 아니면 그냥 본인 쓰기 편한대로 써도 될 것 같다.

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

C# - const, readonly  (0) 2024.08.10
C# - is, as 연산자  (0) 2024.08.07
C# - C#의 자료형  (0) 2024.07.24
C# - 클래스와 콘솔 출력  (2) 2024.07.24
C# - C#과 .NET 프레임워크  (2) 2024.07.23

C#과 C/C++ 은 사용하는 자료형이 다소 다르다.

비슷한 부분이 많지만, 약간의 다른 부분이 있기 때문에 알아둬야 한다.

 

먼저, C#에서 사용하는 기본 자료형의 종류를를 알아보자.

 

정수형

자료형 형식 크기, 저장 방식 범위
sbyte System.Sbyte 8bit (1byte) 
부호 있는 정수
-128 ~ 127
byte System.Byte 8bit (1byte)
부호 없는 정수
0 ~ 255
short System.Int16 16bit (2byte)
부호 있는 정수
-32,768 ~ 32,767
ushort System.UInt16 16bit (2byte)
부호 없는 정수
0 ~ 65,535
int System.Int32 32bit (4byte)
부호 있는 정수
-2,147,483,648 ~ 2,147,483,647 
uint System.UInt32 32bit (4byte)
부호 없는 정수
0 ~ 4,294,967,295
long System.Int64 64bit (8byte)
부호 있는 정수
-9,223,372,036,854,775,808 ~ 
 9,223,372,036,854,775,808 
ulong System.UInt64 64bit (8byte)
부호 없는 정수
0 ~ 18,446,744,073,709,551,615

실수형

자료형 형식 크기, 저장 방식 범위
float System.Single 32bit (4byte)
부호 있는 실수
(부동 소수점)
±1.5e-45 ~ ±3.4e38
double  System.Double 64bit (8byte)
부호 있는 실수
(부동 소수점)
±5.0e-324 ~ ±1.7e308
decimal System.Decimal 126bit (16byte)
부호 있는 실수
(고정 소수점)
±1.0 × 1028  ±7.9 × 1028

문자형

자료형 형식 크기, 저장 방식 범위
char System.Char 16bit (2byte)
유니코드 문자
U+0000 ~ U+FFFF
string System.String 가변적인 크기
유니코드 문자열
 

논리형

자료형 형식 크기, 저장 방식 범위
bool System.Boolean 8bit (1byte) true, false

 

자료형과 형식

위의 표를 보면, 자료형과 형식을 나눠서 설명하고 있다.

코드를 작성하면 아래와 같이 자료형대로 선언할 수도 있고, 형식대로 선언할 수도 있다.

아래 그림과 같이 자료형에 마우스를 대보면, uint도 System.UInt32라는 구조체로 나오고 System.UInt32도 동일하게 나온다. 둘은 실제로 동일한 기능을 하는 것이다.  System.UInt32위에 마우스를 대면 이름을 단순화 할 수 있다는 경고 표시가 뜬다. uint로 쓰나, System.UInt32로 쓰나 기능은 어차피 똑같으니까 더 짧은 uint를 쓰라는 의미인 듯 하다.

굳이 누가 진짜인지를 따지자면, System.UInt32가 본래 이름이고, uint가 별칭이다.

이렇게 기본 자료형에도 다양한 멤버함수가 포함되어 있어서 편하게 프로그래밍을 할 수 있다.

부동 소수점, 고정 소수점

위의 표를 보면, 실수를 표현하는 자료형은 float, double, decimal 총 3가지가 있다.

보면, float과 double는 부동 소수점 방식이지만, decimal은 고정 소수점 방식이다.

 

부동 소수점과 고정 소수점을 이 게시글에서 자세하게 설명하진 않을 것이다.

다만, 간단한 차이는 알고 가자.

 

고정 소수점 방식은 부동 소수점 방식에 비해 정밀하게 소수를 표현할 수 있지만, 부동 소수점 방식에 비해 표현할 수 있는 범위가 적다. 

 

반면 부동 소수점 방식은 고정 소수점 방식에 비래 넓은 범위를 표현할 수 있지만, 고정 소수점 방식 보다 정밀하지 않다.

 

고정 소수점 방식의 표현 범위를 넓히기 위해선, 메모리 공간 자체를 크게 할당해야 한다. 그렇기 때문에 C#에서 사용하는 고정 소수점 방식의 실수 자료형인 decimal은 16byte라는 어마어마한 크기를 차지하고 있는 것이다. decimal은 정밀한 연산에 사용할 수 있지만, 메모리를 많이 차지하고 연산 속력 또한 float, double에 비해 느리다.

 

금융, 시뮬레이션 등의 정밀한 소수 계산이 필요한 상황이 아니라면 float, double로도 충분하기 때문에 가능하다면 float과 double를 사용하는 것이 성능상 좋을 것이다.

 

문자형

C++에선 아스키코드로 문자를 저장하는 char와 unsigned char가 있다. 반면, C#은 아스키코드로만 처리하는 자료형은 따로 없고, 모든 문자를 유니코드로 처리한다.

 

char은 'A' 와 같이 문자 하나를 저장할 때 사용한다. 유니코드 방식이기 때문에 '김', '최' 등의 한글도 저장할 수 있다.

string은 문자열을 저장하는 타입이다. std::wstring과 비슷하다고 보면 된다.

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

C# - const, readonly  (0) 2024.08.10
C# - is, as 연산자  (0) 2024.08.07
C# - 클래스, 접근 제한 지정자, 프로퍼티  (0) 2024.07.27
C# - 클래스와 콘솔 출력  (2) 2024.07.24
C# - C#과 .NET 프레임워크  (2) 2024.07.23

C++은 main함수를 작성하고 출력을 하기 위해선 iostream 헤더파일을 추가한 뒤 아래와 같이 코드를 작성해야 한다.

#include <iostream>

int main()
{
    std::cout << "Hello World!" << std::endl;
    return 0;
}

하지만, C#은 다르다. 헤더파일을 추가할 필요도 없고, main 함수도 클래스 내부에 속해야 한다.

일단 코드를 먼저 보자. C#은 아래와 같이 작성된다.

using Sysyem;

public class MainClass
{
    static int Main()
    {
        Console.WriteLine("Hello World!");
        return 0;
    }
}

먼저,  눈에 띄는 것은 System이라는 네임스페이스이다.

 

해당 네임스페이스 안에는 Write, WriteLine와 같은 콘솔출력 함수도 있고 그 외에도 닷넷 프레임워크 시스템 단위에서 지원해주는 많은 기능들이 있다. C++의 std와 유사하다고 보면 될 것 같다.

 

C#은 C++과 달리 헤더파일이라는 개념이 없기 때문에 특별한 전처리기 구문 없이도 기능을 사용할 수 있다. System 네임스페이스에 있는 기능을 그냥 가져다 쓰면 된다.

 

그 다음으로 볼 것은 진입점 함수(Main 함수)가 클래스 내부에 있다는 것이다. C#은 C++과 달리 완전한 객체지향 언어이기 때문에 클래스 외부에 함수가 존재할 수 없다. 그렇기 때문에 진입점 함수도 클래스 내부에 선언해야 한다.

 

하지만, 클래스 내부에 진입점 함수를 선언하게 되면 한 가지 문제가 있다. 일반적인 멤버함수는 인스턴스가 생성되지 않으면 호출할 수 없다는 것이다. 즉, 프로그램이 실행되었는데도 Main함수에 접근할 수 없다는 의미이다.

(객체를 생성하려면 객체 생성 코드를 실행해야 하는데, 가장 처음으로 실행되는 코드가 Main 함수 호출 코드이기 때문에 Main 함수를 호출할 수 없게 된다.)

 

이러한 문제를 해결하기 위해 Main 함수는 반드시 static 함수로 선언해야 한다.

(위에 코드를 보면 static으로 선언되어 있다.)

 

마지막으로 살펴볼 것은 WriteLine함수이다. WriteLine는 C의 printf나 C++의 std::cout 과 유사한 기능을 가지고 있다. 입력받은 문자열을 콘솔에 출력하는 기능이다. 다만, WriteLine 함수는 자동으로 개행(줄바꿈)문자를 문자열의 끝에 삽입해준다.

 

WriteLine("안녕");

WriteLine("잘가");

 

라고 작성한다면, 콘솔에는 아래와 같이 출력된다.

 

C#에는 WriteLine말고 Write라는 함수도 있다.

Write함수는 개행문자를 삽입하지 않는다. printf나 cout과 동일한 결과를 보인다는 것이다.

 

Write("안녕");

Write("잘가");

 

와 같이 코드를 작성한다면 콘솔에는 아래와 같이 출력된다.

 

또한, C++과 차이를 하나 더 보자면, C++은 대부분의 함수가 소문자로만 작명되어있다.반면, C#은 파스칼 케이싱(단어의 앞문자를 모두 대문자로 표기)을 사용하는 듯 하다. 필자처럼 파스칼 케이싱을 자주 사용하는 사람이라면 C#이 꽤나 잘 맞을 수도 있다.

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

C# - const, readonly  (0) 2024.08.10
C# - is, as 연산자  (0) 2024.08.07
C# - 클래스, 접근 제한 지정자, 프로퍼티  (0) 2024.07.27
C# - C#의 자료형  (0) 2024.07.24
C# - C#과 .NET 프레임워크  (2) 2024.07.23

 

비주얼 스튜디오에서 빈 프로젝트를 만들려고 보니, C#은 저렇게 (.NET Framework)가 뒤에 붙어있는 것이 보인다. C++에는 .NET Framework가 붙어있지 않은 것을 보면 C#은 C++과 달리 .NET Framework 라는 것과 긴밀한 연관이 있다는 것을 알 수 있다.

 

그렇다면, .NET Framework는 무엇이고, C#은 .NET Framework 와 무슨 관련이 있을까?

 

.NET Framework (닷넷 프레임워크)

닷넷 프레임워크는 마이크로 소프트에서 제작한 개발 및 실행 환경이다. 프로그래머는 닷넷 프레임워크가 제공해주는 틀 위에서 자유롭게 프로그래밍을 할 수 있으며, 닷넷 프레임워크 위에서 작동하는 대표적인 언어가 C#인 것이다.

 

닷넷 프레임워크는 단일 언어를 지원하는 것이 아니라 비주얼 베이직, J#등 여러 언어를 지원한다. 즉, 여러 언어에 대해 동일하게 작동하기 위해서 닷넷 프레임워크는 특수한 방식으로 동작한다. 동작 원리를 먼저 간단하게 보면, 아래와 같다.

 

1. 프로그래머에 의해 작성된 코드가 컴파일러에 의해 IL(intermediate Language)로 번역된다.

2.  IL로 번역된 코드와 라이브러리와 링크되어 exe파일, dll파일로 저장된다.

3. exe파일을 실행하게 되면, CLR(common Language Runtime)에 의해 실시간으로 IL이 기계어로 번역되며 실행된다. 

 

아래 그림은 위의 과정을 그림으로 표현한 것이다. 

출처 : https://wikidocs.net/227163

 

 

닷넷 프레임워크 위에서 작성된 코드는 컴파일러에 의해 CIL(IL) 이라는 공용 언어로 번역된다.

이 공용 언어는 CLR이라는 프로그램에 의해 기계어로 번역된다.

 

그렇다면, 왜 이러한 과정을 거칠까?

 

먼저, CIL이란 닷넷 프레임워크에서 사용하는 언어이다. OS가 아닌 프로그램에서 위에서 사용되는 언어이기 때문에 OS에  의존적이지 않다. (OS가 달라도 닷넷 프레임워크가 설치되어 있다면 작동한다는 뜻)

 

하지만, 닷넷 프레임워크 위에서 동작하는 코드도 결국엔 기계어로 번역이 되어야 운영체제가 이를 이해하고 하드웨어에 연산을 요청할 수 있다. 그렇기 때문에 CIL은 CLR이라는 것에 의해 실행 환경에 맞는 기계어로 번역이 된다.

(실제로는 JIT라는 컴파일러가 번역을 수행하고, CLR은 이를 요청하는 것)

 

이 과정을 통해, 닷넷 프레임워크 위에서 작성된 여러 언어들이 통일성있게 실행될 수 있으며, 닷넷 프레임워크만 설치되어 있다면 개발, 실행 환경이 달라지더라도 문제없이 프로그램을 실행할 수 있게 된다.

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

C# - const, readonly  (0) 2024.08.10
C# - is, as 연산자  (0) 2024.08.07
C# - 클래스, 접근 제한 지정자, 프로퍼티  (0) 2024.07.27
C# - C#의 자료형  (0) 2024.07.24
C# - 클래스와 콘솔 출력  (2) 2024.07.24

+ Recent posts