본문 바로가기

유니티 스파르타 캠프 2주차

Unity C# 기초 문법 중 어려운 내용 간단 정리

🔴 오늘의 내용

  1. 일정과 바뀐 내용
  2. C# 1주차 ~ 3주차 강좌
  3. 후기 및 내일 일정

1. 바뀐 일정

오늘은 주말을 보내고 와서 그런지 기존의 일정대로 진행하며 직접적으로 코드를 작성하는 것이 꺼려졌었다. 마침 개인 과제가 있다는 이야기를 듣고 C# 문법만 공부했고 그에 대한 거부감이 들지 않을 수 있었다.

 

2. C# 1주차 ~ 3주차 강좌

오늘은 하루를 투자해서 들은 강좌 내용 중에 실질적으로 사용되는 것들을 이해가 쉽게 작성해보려고 한다. 튜터 님께서도 쉽게 풀이해주셨는데 그 중에서도 핵심적인 부분만 적어보고자 한다. 오늘 다룰 내용은 다음과 같다.

  1. 1주차
    1. 코드 작성 시에 알고 있으면 좋은 단축키 (visual studio 2022 기준)
    2. 코드 컨벤션( 개발자들의 코드 작성 규칙 )
    3. 포멧팅 (변수 가져다 쓰는 2가지 방법)
  2. 2주차
    1. 배열을 활용한 맵 구현
    2. 컬렉션의 종류와 구조
    3. 오버로딩
  3. 3주차
    1. 객체지향 프로그래밍의 특징(Unity의 특징)
    2. 생성자와 소멸자(생성될 때, 삭제될 때)
    3. 프로퍼티 ( 보안성, 접근 제한하기 )
    4. 다형성
    5. 제너릭 (모든 타입을 다 받을 수 있는 에 대하여)
    6. out, ref (입력한 값이 메서드에서 변환됨. 넣은 값 자동 Return)

 

[1주차] 1-1. 코드 작성 시에 알고 있으면 좋은 단축키(visual studio 2022 기준)

ctrl k + ctrl c 주석 처리
ctrl k + ctrl u 주석 제거
shift + delete 줄 삭제
tab 줄 이쁘게 만들기
alt + 방향키 코드 줄 이동
F9 디버그 모드
(디버그 설정한 위치 바로 전까지 코드 출력 후 멈춤)
F10 디버그 모드에서 한 줄씩 실행

이정도만 알고 있어도 사용하기 편하다.

 

[1주차] 1-2. 코드 컨벤션 ( 개발자들의 코드 작성 규칙 )

PascalCase 클래스, 메서드(함수)의 첫글자 및 단어의 첫글자는 대문자. 예시) Food, MethodName, KoreaName
camelCase 변수의 첫 글자는 소문자, 이후 단어의 첫글자는 대문자. 예시) food, methodName, koreaName
대문자 약어 예외적으로 전체 글자가 모두 대문자인 식별자도 있음 예시) ID, HTTP, FTP

 

[1주차] 1-3. 포멧팅 ( 변수 가져다 쓰는 2가지 방법 )

1. 문자열 형식화 

string name = "John";
int age = 30;
string message = string.Format("My name is {0} and I'm {1} years old.", name, age);

2. 문자열 보간 ( $ 만 붙이면 되기 때문에 훨씬 편하고 가독성이 좋음) 

string name = "John";
int age = 30;
string message = $"My name is {name} and I'm {age} years old.";

 

[2주차] 2-1 배열을 활용한 맵 구현

int[,] map = new int[5, 5] 
{ 
    { 1, 1, 1, 1, 1 }, 
    { 1, 0, 0, 0, 1 }, 
    { 1, 0, 1, 0, 1 }, 
    { 1, 0, 0, 0, 1 }, 
    { 1, 1, 1, 1, 1 } 
};

for (int i = 0; i < 5; i++)
{
    for (int j = 0; j < 5; j++)
    {
        if (map[i, j] == 1)
        {
            Console.Write("■ ");
        }
        else
        {
            Console.Write("□ ");
        }
    }
    Console.WriteLine();
}

 

[2주차] 2-2 컬렉션의 종류와 구조

1) List

List<int> numbers = new List<int>(); // 빈 리스트 생성
numbers.Add(1); // 리스트에 데이터 추가
numbers.Add(2);
numbers.Add(3);
numbers.Remove(2); // 리스트에서 데이터 삭제

foreach(int number in numbers) // 리스트 데이터 출력
{
    Console.WriteLine(number);
}

배열과는 달리 동적 할당으로 크기를 계속하여 늘릴 수 있다.

 

2) Dictionary

using System.Collections.Generic;

Dictionary<string, int> scores = new Dictionary<string, int>(); // 빈 딕셔너리 생성
scores.Add("Alice", 100); // 딕셔너리에 데이터 추가
scores.Add("Bob", 80);
scores.Add("Charlie", 90);
scores.Remove("Bob"); // 딕셔너리에서 데이터 삭제

foreach(KeyValuePair<string, int> pair in scores) // 딕셔너리 데이터 출력
{
    Console.WriteLine(pair.Key + ": " + pair.Value);
}

키와 값으로 구성된 데이터를 저장한다. 중복된 키는 가질 수 없으며, 키와 값의 쌍을 이루어 데이터를 저장한다.

 

3) Stack

Stack<int> stack1 = new Stack<int>();  // int형 Stack 선언

// Stack에 요소 추가
stack1.Push(1);
stack1.Push(2);
stack1.Push(3);

// Stack에서 요소 가져오기
int value = stack1.Pop(); // value = 3 (마지막에 추가된 요소)

후입 선출의 구조, 마지막에 들어온 녀석이 선발된다. ex) 나중에 온 돌이 박힌 돌 빼낸다.

 

4) Queue

Queue<int> queue1 = new Queue<int>(); // int형 Queue 선언

// Queue에 요소 추가
queue1.Enqueue(1);
queue1.Enqueue(2);
queue1.Enqueue(3);

// Queue에서 요소 가져오기
int value = queue1.Dequeue(); // value = 1 (가장 먼저 추가된 요소)

선입 선출의 구조, 오래 버티는 녀석이 선발된다.

 

5) HashSet

HashSet<int> set1 = new HashSet<int>();  // int형 HashSet 선언

// HashSet에 요소 추가
set1.Add(1);
set1.Add(2);
set1.Add(3);
set1.Add(2); // 이미 존재하므로 추가되지 않음.

set1.Contains(4); 	// 해시셋에 4가 있는지 확인. 있으면 true, 없으면 false.
			// 조합류 게임에서 존재여부 판단할 때 메모리에서 유리할 느낌.

// HashSet에서 요소 가져오기
foreach (int element in set1)
{
    Console.WriteLine(element);
}

중복되지 않은 요소들로 이루어짐. 이 코드에서는 알 수 있는게 없지만, 강

 

※ 배열과 리스트 ※

  • 리스트는 동적으로 크기를 조정할 수 있어 배열과는 다르게 유연한 데이터 구조를 구현할 수 있습니다. 하지만, 리스트를 무분별하게 사용하는 것은 좋지 않은 습관입니다.
    1. 메모리 사용량 증가: 리스트는 동적으로 크기를 조정할 수 있어 배열보다 많은 메모리를 사용합니다. 따라서, 많은 데이터를 다루는 경우 리스트를 무분별하게 사용하면 메모리 사용량이 급격히 증가하여 성능 저하를 유발할 수 있습니다.
    2. 데이터 접근 시간 증가: 리스트는 연결 리스트(linked list)로 구현되기 때문에, 인덱스를 이용한 데이터 접근이 배열보다 느립니다. 리스트에서 특정 인덱스의 데이터를 찾기 위해서는 연결된 노드를 모두 순회해야 하기 때문입니다. 이러한 이유로, 리스트를 무분별하게 사용하면 데이터 접근 시간이 증가하여 성능이 저하될 수 있습니다.
    3. 코드 복잡도 증가: 리스트는 동적으로 크기를 조정할 수 있기 때문에, 데이터 추가, 삭제 등의 작업이 배열보다 간편합니다. 하지만, 이러한 유연성은 코드 복잡도를 증가시킬 수 있습니다. 리스트를 사용할 때는 데이터 추가, 삭제 등의 작업을 적절히 처리하는 코드를 작성해야 하므로, 코드의 가독성과 유지보수성이 저하될 수 있습니다.
    따라서, 리스트를 무분별하게 사용하는 것은 좋지 않은 습관입니다. 데이터 구조를 선택할 때는, 데이터의 크기와 사용 목적을 고려하여 배열과 리스트 중 적절한 것을 선택해야 합니다.

[2주차] 2-3 오버로딩

int AddNumbers(int a, int b)
{
    return a + b;
}

int AddNumbers(int a, int b, int c)
{
    return a + b + c;
}

// 메서드 호출
int sum1 = AddNumbers(10, 20);  // 두 개의 정수 매개변수를 가진 메서드 호출
int sum2 = AddNumbers(10, 20, 30);  // 세 개의 정수 매개변수를 가진 메서드 호출

동일한 이름의 메서드를 다양한 매개변수 목록으로 정의하는 개념.
간단히 말해서 함수의 이름은 같지만, 주는 값에 따라서 다르게 반응하도록 제작할 수 있다. 
예시) Item 이라는 함수에 Weapon과 Armor를 준다.

void Item(Armor armor, string Type) //return 값 없음. 
//Type이 없어도 Armor와 Weapon의 클래스가 다르기에 동작에 문제는 없다. 
{
    Console.WriteLine("갑옷 착용?");
    Console.WriteLine("어디에 착용?");
}

void Item(Weapon weapon)
{
    Console.WriteLine("무기 착용?");
}

// 메서드 호출
Item("칼");  // 1개의 타입을 가진 메서드 호출
Item("등딱지", "등")  // 2개의 타입을 가진 메서드 호출

 

[3주차] 3-1. 객체지향 프로그래밍의 특징(Unity의 특징)

캡슐화 타 스크립트에서 접근이 불가능하도록 제한 가능.
상속 클래스를 다른 클래스에 넘기기 가능
다형성 오버로딩, 오버라이딩 같은 특징
추상화 필요한 코드를 만들기 전에 미리 작성 가능.
객체 오브젝트(객체)가 행동함(메서드)

 

[3주차] 3-2. 생성자와 소멸자(생성될 때, 삭제될 때)

class Person
{
    private string name;

// Person이라는 클래스를 불러올 때 기본의 디폴트 값
    public Person()
    {
   	//이름은 비어있는 게 좀 그래서 추가한 것.
    //디폴트 값에는 아무 값도 없음.
    	name = "Unknown";
	}
    
    public Person(string newName) 
    {
        name = newName;
        Console.WriteLine("Person 객체 생성");
    }

    ~Person()
    {
        Console.WriteLine("Person 객체 소멸");
    }
}

 

[3주차] 3-3. 프로퍼티 (보안성, 접근 제한하기)

//기본, 자동 프로퍼티
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

아래와 같이 값을 받을 때는 제한이 없으나, 저장할 때는 제한을 하도록 만들 수도 있음. 그 반대도 가능.

public string Name
{
    get { return name; }
    //private와 같이 제한 가능.
    private set { name = value; }
}

 

[3주차] 3-4. 다형성

1) 가상 메서드

public class Unit
{
    public virtual void Move()
    {
        Console.WriteLine("두발로 걷기");
    }

    public void Attack()
    {
        Console.WriteLine("Unit 공격");
    }
}

public class Marine : Unit
{

}

public class Zergling : Unit
{
    public override void Move()
    {
        Console.WriteLine("네발로 걷기");
    }
}

위 코드는 Move() 함수를 상속받았지만, 값을 바꿔준 코드입니다. 
virtual의 경우, 그냥 써도 되고 만약 상속받은 클래스가 수정할 의도가 있다면 override로 수정가능합니다.

2) 추상 클래스

abstract class Shape
{
    public abstract void Draw();
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }
}

class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}

class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a triangle");
    }
}

위 코드에서 쓰인 abstract의 경우, 반드시 그 클래스를 만들 것이다. 라고 명시하는 구문입니다. 
마찬가지로 abstract가 붙은 함수 또한 반드시 만들어야합니다. 저는 약간 기획할 때 쓰는 느낌으로 이해했습니다. 

3) 오버라이딩

public class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing a shape.");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle.");
    }
}

public class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a rectangle.");
    }
}

Shape shape1 = new Circle();
Shape shape2 = new Rectangle();

shape1.Draw();  // Drawing a circle.
shape2.Draw();  // Drawing a rectangle.

이미 부모(Shpae) 타입으로 정의된 함수를 자식으로 재정의하는 것을 오버라이딩이라고 합니다.
아마도 자식으로 써야만 하는 상황일 때, 재정의하는 느낌인 거 같습니다. 

 

[3주차] 3-5. 제너릭 (모든 타입을 다 받아서 변환)

// 제너릭 클래스 선언 예시
class Stack<T>
{
    private T[] elements;
    private int top;

    public Stack()
    {
        elements = new T[100];
        top = 0;
    }

    public void Push(T item)
    {
        elements[top++] = item;
    }

    public T Pop()
    {
        return elements[--top];
    }
}

// 제너릭 클래스 사용 예시
Stack<int> intStack = new Stack<int>();
intStack.Push(1);
intStack.Push(2);
intStack.Push(3);
Console.WriteLine(intStack.Pop()); // 출력 결과: 3

제너릭 클래스는 <T> 와 같이 선언하여, 어떤 값이든 받아서 사용할 수 있는 클래스입니다. 

class Pair<T1, T2>
{
    public T1 First { get; set; }
    public T2 Second { get; set; }

    public Pair(T1 first, T2 second)
    {
        First = first;
        Second = second;
    }

    public void Display()
    {
        Console.WriteLine($"First: {First}, Second: {Second}");
    }
}

위와 같이 2개를 선언할 수도 있고, <>를 통해 제너릭임을 표시합니다.  

Pair<int, string> pair1 = new Pair<int, string>(1, "One");
pair1.Display();

Pair<double, bool> pair2 = new Pair<double, bool>(3.14, true);
pair2.Display();

오버로딩의 경우에는 함수를 각각 타입별로 선언해줘야하지만, 제너릭의 경우 하나로 여러 개를 소화가능합니다. 단, 줘야하는 값이 일정할 때에는 제너릭보다는 오버로딩이 안전할 거 같습니다.

 

[3주차] 3-6. out, ref (입력한 값이 메서드에서 변환. 넣은 값이 return)

// out 키워드 사용 예시
void Divide(int a, int b, out int quotient, out int remainder)
{
    quotient = a / b;
    remainder = a % b;
}

int quotient, remainder;
Divide(7, 3, out quotient, out remainder);
Console.WriteLine($"{quotient}, {remainder}"); // 출력 결과: 2, 1

// ref 키워드 사용 예시
void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}

int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine($"{x}, {y}"); // 출력 결과: 2, 1
  out  ref
주어진 값 활용 활용하지 않을 경우, 오류 활용하지 않아도 문제 없음 
값의 변화 함수에 들어간 입력 값(매개변수)이 바뀝니다. 
이점 값에 대한 복사 없이 메서드 내에 직접 접근하기에 성능상 이점이 있습니다.
단점 너무 많은 매개변수 사용 시 코드의 가독성이 떨어지고 유지보수가 어려워집니다. 

 

3. 후기 및 다음 일정

오늘은 배운 내용을 소화하는 것도 힘들 거 같다. 소화할 수만 있다면, 직접 코드를 짜며 문제점을 찾고 배워가는 거 못지 않게 귀중한 지식을 얻은 것 같다. 

다음 일정으로는 C# 남은 강좌를 마저보고 개인 과제를 해보는 것이 될 것으로 생각된다. 2024-04-11에 계획했던 일정은 개인 과제를 마무리하고 작업하면 지금보다도 더 완성도 있는 게임을 제작할 수 있을 것으로 기대된다.