1. 파일 입출력 이란? ( File Input/Output )


C#을 콘솔로 공부하면서 우리는 콘솔창에 많은 메시지를 출력 해왔습니다.

이와 마찬가지로 우리가 필요한 정보를 파일로 하드 디스크에 입출력(읽기, 쓰기)하는 것입니다.

프로그램을 제작하다 보면 코드가 많아진 상황에서 디버그나 필요한 정보의 로그파일(logfile) 즉, 로그를 남겨 정상적인

작동을 하고있는지 체크를 하기도하고 특정 파일형태로 게임 데이터를 저장(SaveFile)하여 불러오기도 합니다.

우선 가장 기본적인 txt 파일을 사용해보도록 하겠습니다. 윈도우에서 메모장을 열어 글을 쓰고 저장을 하면 기본이되는

파일형식인데요. 프로그램에서도 System.IO 라는 네임스페이스로 파일 입출력의 기능을 지원해주고 있습니다.

System.IO 의 구성을 나열하기에는 글이 너무 길어져 MSDN에 기술되어 있는 문서를 참고해주세요.


MSDNhttps://docs.microsoft.com/ko-kr/dotnet/api/system.io?view=netframework-4.7.2


2. 디렉토리 ( Directory )


파일을 읽고 쓰기전 우리는 우선 파일경로에 대해 알아야 합니다. 파일을 읽고 쓰는데 가장 기본적인 오류가 발생하는

곳으로 우리가 저장하고 불러올 파일이 정확히 어디있는지 혹은 어디에 저장할지 정할줄 알아야 한다는 것이지요.


소스코드

using System;
using System.IO;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            //현재 경로
            string currentPath = Directory.GetCurrentDirectory();
            currentPath += "\\Save";

            //특정 경로를 지정하고 싶다면 
            //string currentPath = @"c:\test";
            //string currentPath = "c:\\test";
            //@ 심벌은 문자열 앞에 사용하면 해당 문자열 안의 Escape 문자를
            //무시하고 문자 그대로 인식하기때문에 \\ 보다 자연스럽게 \로 패스를 지정할수 있습니다.


            //현재 경로에 Save 폴더가 존재하는지 확인
            if (Directory.Exists(currentPath))
            {
                Console.WriteLine("경로 존재");
            }
            else
            {
                //디렉토리 생성
                Directory.CreateDirectory(currentPath);
                Console.WriteLine(currentPath + " 에 폴더 생성!");
            }
        }
    }
}

첫 실행 결과

...\...\bin\Debug\Save 에 폴더 생성!

두번째 실행 결과

경로 존재


Directory

 - GetCurrentDirectory() : 프로그램 실행파일이 있는 경로를 가져옵니다.

 - Exists( [파일경로] ) : 파일경로 존재 유무에 따라 bool 타입으로 반환됩니다.

 - CreateDirectory ( [파일경로] ) : 해당 파일경로에 폴더를 생성 합니다. 


주석의 내용과 같이 @ 심벌의 사용 유무는 문자열 안의 Escape 문자 인식 여부에 따라 경로를 지정할수 있습니다.



3. 파일 입출력


파일을 입출력 하기 위해서는 두번째 네임스페이스( System.Text )가 필요합니다.

이는 인코딩(Encoding)을 쓰기 위함인데 인코딩은 정보의 형태나 형식을 변환하는 처리를 말합니다.

쉽게말해 문자를 구성하는 표준 코드를  만들어 사람간 또는 사람과 컴퓨터간 정보를 교환하기 위해 사용합니다.


소스코드

using System;
using System.IO;
using System.Text;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            //현재 경로
            string currentPath = Directory.GetCurrentDirectory();
            currentPath += "\\Save";

            //특정 경로를 지정하고 싶다면 
            //string currentPath = @"c:\test";
            //string currentPath = "c:\\test";
            //@ 심벌은 문자열 앞에 사용하면 해당 문자열 안의 Escape 문자를
            //무시하고 문자 그대로 인식하기때문에 \\ 보다 자연스럽게 \로 패스를 지정할수 있습니다.


            //현재 경로에 Save 폴더가 존재하는지 확인
            if (Directory.Exists(currentPath))
            {
                Console.WriteLine("경로 존재");
            }
            else
            {
                //디렉토리 생성
                Directory.CreateDirectory(currentPath);
                Console.WriteLine(currentPath + " 에 폴더 생성!");
            }

            //생성 파일의 이름과 형식(txt)을 적용합니다.
            string filePath = currentPath + "\\test.txt";


            //파일 스트림을 만든다. 
            FileStream fileStream = new FileStream(
                filePath,              //저장경로
                FileMode.Create,       //파일스트림 모드
                FileAccess.Write       //접근 권한
                );

            //파일스트림 모드
            //FileMode.Create           //파일을 만든다 있으면 덮어 씀.
            //FileMode.CreateNew        //파일을 만든다 있으면 IOException 예외가 발생.
            //FileMode.Append           //파일을 만든다 있으면 뒤에서 부터 추가로 씀.

            //FileMode.Open             //파일을 연다 없다면 FileNotFoundException 예외 발생
            //FileMode.OpenOrCreate     //파일을 연다 없으면 만듬. ( 문제가 있으니 쓰지말자 )
            //FileMode.Truncate         //파일을 연다 없으면 만듬. ( 열든 만들든 무조건 빈파일로 )

            string strData = "오늘도 강좌를 읽어 주셔서 감사합니다.\r\n블로그에 자주 찾아와 주세요.\r\n";

            
            //
            // 방법 1 ( StreamWriter 를 이용한방법 )
            //

            //파일스트림에 무언가를 써주는 StreamWriter 만들기
            StreamWriter streamWriter = new StreamWriter(
				fileStream,            //쓸 파일스트림을 여기에다가.....
				Encoding.UTF8          //파일에다 쓸때 인코딩 객체 전달.
				);


			streamWriter.Write(strData);
            
            //스트림 Writer 닫아 주세요.
            streamWriter.Close();

            /*
            //
            // 방법 2 ( Encoding 을 통해 byte 배열로 쓰는법 )
            //

            byte[] byteArr = Encoding.UTF8.GetBytes(strData);

            //파일 스트림에다가 byte 배열을 직접쓰기.
            fileStream.Write(
                byteArr,
                0,
                byteArr.Length);
            */

            //파일스트림을 다 썼다면 반드시 닫아 주세요.
            fileStream.Close();

            Console.WriteLine("파일생성 또는 덮어 쓰기 완료!");
        }
    }
}

결과

경로 존재
파일생성 또는 덮어 쓰기 완료!


실제 파일이 생성되고 문서내용이 정확이 입력되었는지 해당폴더로 가서 직접 파일을 열어보세요.

56줄에 /* 와 72줄 */ 을 추가하고 73줄 /* 85줄 */ 을 삭제하여 방법 2번도 사용해보세요.

다쓴 FileStream, StreamWriter 인스턴스는 반드시 닫아주세요.


위의 코드와 같이 파일을 저장했다면 불러와서 직접 코드내에서 문서의 내용을 확인해 봐야 겠지요?


소스코드

using System;
using System.IO;
using System.Text;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            //현재 경로
            string currentPath = Directory.GetCurrentDirectory();
            currentPath += "\\Save";

            //현재 경로에 Save 폴더가 존재하는지 확인
            if (!Directory.Exists(currentPath))
            {
                //디렉토리 생성
                Directory.CreateDirectory(currentPath);
                Console.WriteLine(currentPath + " 에 폴더 생성!");
            }

            //생성 파일의 이름과 형식(txt)을 적용합니다.
            string filePath = currentPath + "\\test.txt";

            //string sFileName = "\\test.txt";
            string strData = "오늘도 강좌를 읽어 주셔서 감사합니다.\r\n블로그에 자주 찾아와 주세요.\r\n";

            //파일 쓰기 메서드
            FileWrite(filePath, strData);

            //파일 읽기 메서드
            FileRead(filePath);
        }
        
        static void FileWrite(string filePath, string strData)
        {
            //파일 스트림을 만든다. 
            FileStream fileStream = new FileStream(
                filePath,              //저장경로
                FileMode.Create,       //파일스트림 모드
                FileAccess.Write       //접근 권한
                );

            StreamWriter streamWriter = new StreamWriter(fileStream, Encoding.UTF8);
            streamWriter.Write(strData);

            //다쓴 StreamWriter 와 FileStream 닫기
            streamWriter.Close();
            fileStream.Close();
        }
        static void FileRead(string filePath)
        {
            //폴더 경로는 확인하였고 실제 파일이 있는지 유무를 확인
            FileInfo fi = new FileInfo(filePath);
            if(fi.Exists)
            {
                //파일 존재
                //파일 읽어올 스트림 준비
                FileStream fileStream = new FileStream(
                    filePath,
                    FileMode.Open,
                    FileAccess.Read);

                //파일스트림을 읽는 Reader 생성
                StreamReader streamReader = new StreamReader(
                    fileStream,
                    Encoding.UTF8);         //쓰기와 동일하게 UTF8로 읽음.

                //읽어올 파일을 저정할 스트링 빌더 생성
                StringBuilder strBulider = new StringBuilder(1000);  //빌더크기(Capacity) 1000

                //Peek : 파일 끝에 도달하거나 문제가 발생하면 -1을 반환 합니다.
                while (streamReader.Peek() > -1)    //파일의 끝이 아닐동안계속
                {
                    string strLine = streamReader.ReadLine();      //파일 한줄 읽어온다.

                    //읽어온내용 문자열 추가
                    //strBulider.Append(strLine);

                    //읽어온내용 문자열로 추가하고 줄바뀜
                    strBulider.AppendLine(strLine);

                }

                //스트링 빌더에 있는 내용 출력
                Console.WriteLine(strBulider.ToString());

                //다쓴 StreamReader 와 FileStream 닫기
                streamReader.Close();
                fileStream.Close();
            }
            else
            {
                //파일 없음
                Console.WriteLine("읽을 파일이 없습니다.");
            }
        }
    }
}

결과

오늘도 강좌를 읽어 주셔서 감사합니다.
블로그에 자주 찾아와 주세요.


코드가 길어져서 읽기와 쓰기를 분할하여 메서드에 옴겨 놓았습니다.

파일 일기코드는 52줄에서 98줄을 참고해주세요.


파일쓰기와 마찬가지로 파일의 존재 유무에 대해 알 필요성이 있어 FileInfo 클래스를 이용하여 확인하고

파일을 받아들이는 FileStream은 동일하게 사용 하였습니다. 차이점은 쓰기는 StreamWriter를 이용하고 읽기는

StreamReader를 이용합니다. 또한 문서의 끝을 알기위해 Peek() 메서드를 활용하여 문서의 끝에 도달하면 반복문을

빠져나오는 구조 입니다. 읽어 들이는 방법은 여러가지 형태가 있으며 상황에 맞게 사용합니다.

소스코드에서는 한줄씩 읽어 들이는 방법으로 사용하였고 처리 또한 긴 문서가 아니여서 간단하게 처리되었습니다.


이상으로 파일 입출력 강좌를 마치도록 하겠습니다.



추가 내용

소스코드

using System;
using System.IO;
using System.Text;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            //현재 경로
            string currentPath = Directory.GetCurrentDirectory();
            currentPath += "\\Save";

            //현재 경로에 Save 폴더가 존재하는지 확인
            if (!Directory.Exists(currentPath))
            {
                //디렉토리 생성
                Directory.CreateDirectory(currentPath);
                Console.WriteLine(currentPath + " 에 폴더 생성!");
            }

            //생성 파일의 이름과 형식(txt)을 적용합니다.
            string filePath = currentPath + "\\test.txt";

            // 우리는 다음과 같은 규칙을 가지고 데이터를 저장합니다.
            // [데이터 이름]=[값]
            string strData = "hp=10\nmp=20\n";

            //파일 쓰기 메서드
            FileWrite(filePath, strData);

            //파일 읽기 메서드
            FileRead(filePath);
        }
        
        static void FileWrite(string filePath, string strData)
        {
            //파일 스트림을 만든다. 
            FileStream fileStream = new FileStream(
                filePath,              //저장경로
                FileMode.Create,       //파일스트림 모드
                FileAccess.Write       //접근 권한
                );

            StreamWriter streamWriter = new StreamWriter(fileStream, Encoding.UTF8);
            streamWriter.Write(strData);

            //다쓴 StreamWriter 와 FileStream 닫기
            streamWriter.Close();
            fileStream.Close();
        }
        static void FileRead(string filePath)
        {
            //폴더 경로는 확인하였고 실제 파일이 있는지 유무를 확인
            FileInfo fi = new FileInfo(filePath);
            if(fi.Exists)
            {
                //파일 존재
                //파일 읽어올 스트림 준비
                FileStream fileStream = new FileStream(
                    filePath,
                    FileMode.Open,
                    FileAccess.Read);

                //파일스트림을 읽는 Reader 생성
                StreamReader streamReader = new StreamReader(
                    fileStream,
                    Encoding.UTF8);         //쓰기와 동일하게 UTF8로 읽음.

                //읽어올 파일을 저정할 스트링 빌더 생성
                StringBuilder strBulider = new StringBuilder(1000);  //빌더크기(Capacity) 1000

                //Peek : 파일 끝에 도달하거나 문제가 발생하면 -1을 반환 합니다.
                while (streamReader.Peek() > -1)    //파일의 끝이 아닐동안계속
                {
                    string strLine = streamReader.ReadLine();      //파일 한줄 읽어온다.

                    //읽어온내용 문자열 추가
                    //strBulider.Append(strLine);

                    //읽어온내용 문자열로 추가하고 줄바뀜
                    strBulider.AppendLine(strLine);

                }

                //스트링 빌더에 있는 내용 출력
                Console.WriteLine(strBulider.ToString());

                //다쓴 StreamReader 와 FileStream 닫기
                streamReader.Close();
                fileStream.Close();

                //추가) 정한 규칙으로 저장된 데이터를 파싱 합니다.
                int hp = 0;
                int mp = 0;

                string strTemp = strBulider.ToString();

                //string의 split메서드를 활용하여 개행문자 '\n' 을 기준으로 기존의 문자열을
                //나누어 3개의 배열로 반환됩니다.
                string[] data = strTemp.Split('\n');        
                
                for (int i = 0; i < data.Length; i++)
                {
                    //data[0]는 "hp=10" data[1]는 "mp=20" data[2] = "" 형태의 문자열로 저장되어 있습니다.

                    if(data[i].Length > 0)  //반환된 배열중에 데이터가 없는 배열은 제외(data[2])
                    {
                        string TempData = data[i];              

                        // 우리는 "데이터 이름=값" 형식으로 저장하였기에 다시한번 split로 문자'=' 를 기준으로 나누면
                        // 0번 배열은 데이터 이름, 1번 배열은 값을 가진다는
                        // 규칙을 이용하여 데이를 파싱 할수 있습니다.
                        string[] result = TempData.Split('='); 

                        if (result[0] == "hp")
                            hp = Convert.ToInt32(result[1]);  // convert 클래스는 문자열을 원하는 형태로 변환합니다.
                        if(result[0] == "mp")
                            mp = Convert.ToInt32(result[1]);
                    }
                }
                Console.WriteLine("hp = " + hp.ToString());
                Console.WriteLine("mp = " + mp.ToString());
            }
            else
            {
                //파일 없음
                Console.WriteLine("읽을 파일이 없습니다.");
            }
        }
    }
}

추가 코드는 

[데이터 이름] = [값] 이라는 규칙을 가지고 데이터를 저장하고 파싱하여 원하는 데이터로 활용할수 있게 다시 파싱하는 코드를 수정 및 추가 하였습니다.

1. 델리게이트 ( Delegate )


델리게이트는 메서드를 참조하여 대신 일을 하는 대리자라 할수 있습니다.

메서드를 참조한다는 것은 매개변수 대신 메서드를 매개변수로 사용할수 있다는 말입니다.

개별적으로 사용도 가능하고 복수의 메서드들을 사용할수도 있습니다.

사용시 주의해야 될점은 델리게이트와 참조하여 사용하는 메서드들의 구성형식(반환타입, 매개변수...)이 같아야

한다는 것입니다.


서식형식

[한정자] delegate [반환형식] [델리게이트 이름] ( [매개변수] );

소스코드

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Function()
        {
            Console.WriteLine("Function Call!");
        }
        static void Function2()
        {
            Console.WriteLine("Function2 Call!");
        }
        static void Function3()
        {
            Console.WriteLine("Function3 Call!");
        }
        //일을 대신할 델리게이트 참조자를 선언
        delegate void Work();

        static void Main(string[] args)
        {
            //델리게이트 인스턴스 생성(단일 목적으로 사용)
            Work w1 = new Work(Function);
            w1();

            //기존의 참조된 메서드는 해제되고 새로운 메서드를 참조
            w1 = new Work(Function2);
            w1();

            Console.WriteLine();

            // +, -, +=, -= 연산자로 복수의 참조자를 늘릴수도 줄일수도 있습니다.
            w1 = new Work(Function);
            w1 += new Work(Function2);
            w1 += new Work(Function3);
            w1();

            w1 -= Function2;
            Console.WriteLine();
            w1();
        }
    }
}

결과

Function Call!
Function2 Call!

Function Call!
Function2 Call!
Function3 Call!

Function Call!
Function3 Call!


위와 같이 델리게이트는 연산자를 이용하여 추가할수도 삭제할수도 있습니다.

일종의 메서드배열과 같이 델리게이트를 실행하면 참조되고있는 모든 메서드들을 실행하게 됩니다.

또한 델리게이트를 인스턴스화 하여 객체를 매개변수로 전달할수도 있습니다.




2. 이벤트 ( Event )


이벤트는 컴퓨터에서 특정한 일이 일어나게 되면 발생하는 메세지라 할수 있습니다.

일반적으로 많이 사용하는 windows 운영체제 역시 이 이벤트 기반으로 만들어져 있으며 기능들을 수행하게 됩니다.

특정 시간이되면 알려주는 이벤트라던지 사용자가 주변기기를 사용하여 마우스를 이동, 클릭, 드래그 등...

이벤트와 델리게이트의 차이점은 외부에서 직접적인 접근이 가능하냐 그렇지 못하냐의 차이가 있습니다.


서식 형식

[접근 한정자] event [델리게이트 명];

소스코드

using System;

namespace ConsoleApp1
{
    delegate void EventHandler(int number, string message);

    //방문자 이벤트를 발생할 클래스
    class Visitor
    {
        public event EventHandler EventCall;
        
        public void VisitorCheck(int VisitorNum)
        {
            //방문자가 3의 배수일때마다 이벤트 발생
            if (VisitorNum % 3 == 0)
                EventCall(VisitorNum, "방문자");
        }
    }

    //이벤트를 받아 처리할 
    class Celebration
    {
        public void VisitorEvent(int number, string message)
        {
            if (number == 9)
            {
                Console.WriteLine("---축하합니다---");
                Console.WriteLine("{0}번째 {1}로 이벤트에 당첨 되셨습니다.", number, message);
            }
            else
                Console.WriteLine("방문자 이벤트 발생!");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            //이벤트를 받을 객체 인스턴스
            Celebration Cele = new Celebration();

            //이벤트 발생 인스턴스
            Visitor Vis = new Visitor();
            //이벤트가 발생될때 호출될 메서드 추가
            Vis.EventCall += new EventHandler(Cele.VisitorEvent);

            for(int i = 0; i < 10; i++)
            {
                Vis.VisitorCheck(i);
            }
        }
    }
}

결과

방문자 이벤트 발생!
방문자 이벤트 발생!
방문자 이벤트 발생!
---축하합니다---
9번째 방문자로 인벤트에 당첨 되셨습니다.


이러한 이벤트 기반 프로그래밍은 프로그램의 상태변화에 따라 델리게이트를 호출하여 안정적인 데이터 관리와

공정하게 이벤트 상황을 전달할수 있습니다. 외부에서 호출이 가능한 델리게이트는 임의 적으로 호출이 가능하다는 점때문에

프로그램의 상태변화와 상관없이 호출되어 버그를 발생 시킬수 있습니다.


이상으로 델리게이트와 이벤트에 대해서 알아 보았습니다.

수고하셨습니다.

1. 예외 처리 이란 ( Exception Handling)


우리가 인생을 살아가다보면 예상치 못한 일들이 일어나기 마련입니다.

감기에 걸려 출근을 못할수도 있고 물웅덩이에서 물이 튀어 새옷을 버릴수도 있습니다.

이러한 예상치 못한 일들을 겪다보면 자연스럽게 대처하는 방법들이 있습니다.


프로그래밍에서도 마찬가지로 예상치 못한 일들이 일어나기 마련입니다.

더군다나 여러사람이 함께 프로젝트를 진행하다보면 더 많은 일들이 일어나기 마련이지요.

게임을 하던중 예기치 못한 오류가 발생해 프로그램이 종료되는 상황들을 겪어 보셨나요?


이처럼 예외 상황이 발생하였을때 프로그램 종료없이 유연하게 처리하는 것을 예외 처리라 합니다.


2. try~catch, throw, finally


try~catch

앞선 강좌에서 goto 문을 활용하여 어디서든 코드를 점프하여 다음 코드들을 실행하는 것을 접해봤습니다.

이것과 마찬가지로 예외가 발생할 만한 코드들이 있는곳 또는 예외가 발생해서는 안되는 코드에 try 키워드를 

써서 해당 코드들을 블럭( {...} )안에 넣어 놓으면 예외 발생시 예외 상황을 (시도하다) catch 키워드가 있는 곳에서

(받다) 예외상황에 대처할 수 있도록 합니다.


서식 형식

try
{
    //....
}
catch( [예외 객체 1] )
{
}
catch( [예외 객체 2] )
{
}
...


소스코드
using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] arr = { 1, 2, 3 };

            try
            {
                //int[] 배열의 크기를 넘은 인덱스를 사용하여 예외 발생
                arr[4] = 4;
            }
            catch (DivideByZeroException e)
            {
                Console.WriteLine("예외 발생 : {0}", e.Message);
            }
            catch (IndexOutOfRangeException e) // 배열의 크기에 대한 예외 상황 발생시 받는 곳
            {
                Console.WriteLine("예외 발생 : {0}", e.Message);
            }

            try
            {
                //상수를 0으로 나누려하여 예외 발생
                int a = 10;
                a = a / 0;
            }
            catch (IndexOutOfRangeException e)
            {
                Console.WriteLine("예외 발생 : {0}", e.Message);
            }
            catch (DivideByZeroException e) // 0으로 나누기를 하였을때 예외 상황을 받는 곳
            {
                Console.WriteLine("예외 발생 : {0}", e.Message);
            }

            try
            {
                arr[4] = 4;
            }
            //모든 예외상황은 System.Exception 클래스를 상속받아 파생된 클래스입니다.
            //하나의 catch 만으로 모두 처리 할수 있습니다.
            catch(Exception e)
            {
                Console.WriteLine("예외 발생 : {0}", e.Message);
            }
        }
    }
}

결과

예외 발생 : 인덱스가 배열 범위를 벗어났습니다.
예외 발생 : 0으로 나누려 했습니다.
예외 발생 : 인덱스가 배열 범위를 벗어났습니다.


소스코드의 주석의 내용과 같이 모든 예외는 System.Exception으로 부터 파생되어 만들어져 있습니다.

Exception 클래스 만으로 예외상황에 대응 할수 있지만 경우에 따라서는 정확한 처리가 필요할때는 대응이 어려울수

있습니다. 따라서 Exception 만으로 대응 하는것은 자제 하는것이 좋고 올바른 예외 처리를 하기 위해 명확하게 사용하는

것이 좋습니다.


때로는 라이브중인 프로그램에서 최악의 상황(강제 종료)은 피하기위해 예외처리를 설정하고 버그를 수정하기 전까지

유지하는 경우도 있습니다.



throw

catch 키워드로 예외를 받는 다는 것은 어디선가는 예외 상황을 던지고 있다는 말이되고 이는 throw 키워드를 이용하여

직접 예외 상황을 발생 시킬수 있습니다.

다음 소스코드를 참조 하시기 바랍니다.


소스 코드

using System;

namespace ConsoleApp1
{
    class Program
    {
        static int password = 1234;
        static void Main(string[] args)
        {
            try
            {
                CheckPassword(333);
                CheckPassword(1234);
            }
            catch(Exception e)
            {
                Console.WriteLine("예외 발생 : {0}", e.Message);
            }
        }
        static void CheckPassword(int pass)
        {
            if (password != pass)
                throw new Exception("패스워드가 틀립니다.");
            else
                Console.WriteLine("비밀번호 인증완료!");
        }
    }
}

결과

예외 발생 : 패스워드가 틀립니다.


이처럼 프로그래머가 직접 특정 상황에 맞게 예외를 던지고 받을수 있습니다.

 

finally

finally는 try 블럭이 실행된다면 예외가 발생하든 하지 않던 어떤 경우에라도 반듯이 실행되는 키워드 입니다.

할당 받은 100개의 자원들을 사용중에 예외 상황이 발생하여 미처 사용중이던 자원을 해지 하지 못해 사용할수 없는

상황이 발생한다면 사용 할수있는 자원의 갯수는 줄어들고 잠재적 버그를 가지고 사용하게 되는 상황이 발생 할수도

있습니다. 그렇다고 모든 catch 문에서 해지하는 코드를 넣는것 또한 바람직 하지 못한 코드가 됩니다.

이럴때 사용하는 것이 finally 입니다.


소스코드

using System;

namespace ConsoleApp1
{
    class Program
    {
        class NameObject
        {
            bool IsUse = false;
            public bool USE
            {
                get { return IsUse; }
            }
            string sName;

            public NameObject(string name)
            {
                sName = name;
            }
            public void objOpen()
            {
                if (!IsUse)
                {
                    IsUse = true;
                    Console.WriteLine("Open : {0}", sName);
                }
                else
                    throw new Exception("사용중인 오브젝트 " + sName);
            }
            public void objClose()
            {
                if(IsUse)
                {
                    IsUse = false;
                    Console.WriteLine("Close : {0}", sName);
                }
            }
        }
        static void Main(string[] args)
        {
            NameObject[] objs = { new NameObject("A"), new NameObject("B"), new NameObject("C") };
            objs[1].objOpen();

            try
            {
                for (int i = 0; i < objs.Length; i++)
                    objs[i].objOpen();
            }
            catch(Exception e)
            {
                Console.WriteLine("예외 발생 : {0}", e.Message);
            }
            finally
            {
                for(int i = 0; i < objs.Length; i++)
                {
                    if (objs[i].USE)
                        objs[i].objClose();
                }
                Console.WriteLine("모든 오브젝트 해제 완료!");
            }
        }
    }
}

결과

Open : B
Open : A
예외 발생 : 사용중인 오브젝트 B
Close : A
Close : B
모든 오브젝트 해제 완료!


finally는 try를 실행하게되면 어떠한 경우라도 예외가 일어나든 일어나지 않든 간에 반드시 실행됩니다.

위의 예제와 같이 마지막에 해제되는 코드를 넣어 둔다면 자연스럽게 안정성을 가지면서 코드를 정리 할수 있습니다. 


이상으로 예외처리를 마치겠습니다.

수고하셨습니다.





1. 인터페이스 (Interface)


오늘날 우리가 많은곳에서 사용하는 usb 포트는 다방면으로 사용 가능한 장치입니다.

어뎁터를 통해 휴대폰을 충전한다 던지 컴퓨터 주변기기들(마우스, 키보드, 프린터, 선풍기...)을 usb 포트를 통해

전원을 공급한다던지 데이터 수신호를 보낸다던지 컴퓨터와 사용자간 연결 통로같은 역활들을 합니다.

이는 다양한 응용력, 확장능력을 가지고 있습니다.


앞서 말한 usb포트와 같은 역활을 하는것이 이번장에 공부할 인터페이스(Interface)입니다.

인터페이스는 클래스와 비슷해 보이지만 메서드, 이벤트, 프로퍼티, 인덱서 등만 가질수 있다는

차이가 있고 접근 한정자는 public으로만 선언 할수 있습니다. 또한 인스턴스를 만들 수없어 직접적인

사용은 불가능하지만 상속을 받는 클래스의 인스턴스를 생성하는 것은 가능합니다.


- 메서드, 이벤트, 프로퍼티, 인덱서 만을 가질수 있습니다.

- 자체적으로 인스턴스화 될수 없으며 상속을 통한 클래스의 인스턴스는 생성가능 합니다.

- 실체화 될수 없는 인터페이스는 추상적인 멤버를 가집니다.

- 접근 한정자는 public으로만 기본적으로 지정됩니다.

- 인스턴스는 참조자 형으로 사용할수 있습니다.


선언 형식

interface [인터페이스명]
{
     [반환형식] [메서드명] ( [매개변수 목록] );
     //...
}

소스코드

using System; namespace ConsoleApp1 { //인터페이스 메서드의 원형으로만 이루어 집니다. //멤버 변수는 존재 할수 없습니다. interface ITest { //접근제한자 붙이지 않는다. void Function(); void Function2(); } interface ITest2 { void Function2(); } //인터페이스 다중상속 class TestClass : ITest, ITest2 { //상속받은 인터페이스의 모든 메서드를 구현하도록 강제 됩니다. public void Function() { Console.WriteLine("하하하하"); } public void Function2() { Console.WriteLine("호호호호"); } }; class Program { static void Main(string[] args) { TestClass tc = new TestClass(); tc.Function(); tc.Function2(); Console.WriteLine(); //인터페이스는 인스턴스화 될수 없지만 참조자 형으로 사용 가능 ITest interfaceA = tc; interfaceA.Function(); interfaceA.Function2(); Console.WriteLine(); ITest2 interfaceB = tc; interfaceB.Function2(); } } }

결과

하하하하
호호호호


하하하하

호호호호






2. 형변환 ( is, as )


앞서 공부한 클래스에서도 사용할수 있는 키워드 입니다.

형식 비교와 형변환을 위해 사용할수 있는 키워드로 상속 클래스, 상속 인터페이스에서 안전성과 명확성이

필요로 할때 사용할수 있습니다.


is : 객체가 해당 형식인지 비교하여 그 결과를 bool 타입으로 반환 합니다.

as : 형변환 연산자가 변환에 실패할 경우 예외(오류)를 던지는 것에 비해 as는 객체 참조를 null로

     만든다는 차이가 있습니다.


인터페이스가 인터페이스를 상속하고 이를 클래스로 상속받아 인스턴스를 구현

using System;

namespace ConsoleApp1
{
    interface IGun
    {
        void Fire();
    }

    interface IAutoGun : IGun
    {
        void Fire(int count);
    }

    interface ILaser
    {
        void shoot();
    }

    class TestClass : IAutoGun, ILaser
    {
        public void Fire()
        {
            Console.WriteLine("IGun : 빵야!");
        }

        public void Fire(int count)
        {
            for (int i = 0; i < count; i++)
            {
                Console.WriteLine("IAutoGun : 뚜루뚜루!");
            }
        }
        public void shoot()
        {
            Console.WriteLine("ILaser : 찌이이잉~!");
        }

    };

    class Program
    {
        static void Main(string[] args)
        {
            TestClass tc = new TestClass();
            tc.Fire();
            tc.Fire(3);
            tc.shoot();
            Console.WriteLine();

            // is 같은 형식인지 비교
            if(tc is IGun)
            {
               // as : 형변환을 이후 안전성 확인
                IGun gun = tc as IGun;
                if (gun != null)
                    gun.Fire();
            }

            if(tc is ILaser)
            {
                ILaser lasergun = tc as ILaser;
                if (lasergun != null)
                    lasergun.shoot();
            }
        }
    }
}

결과

IGun : 빵야!
IAutoGun : 뚜루뚜루!
IAutoGun : 뚜루뚜루!
IAutoGun : 뚜루뚜루!
ILaser : 찌이잉~!

IGun : 빵야!
ILaser : 찌이잉~!





3. 추상 클래스 (abstract)


추상 클래스는 인터페이스와 클래스중 많은 부분이 클래스에 더 가깝습니다.

메서드, 필드, 메서드 구현, 프로퍼티, 이벤트 등을 가질수 있고 인터페이스와 달리 직접 구현 또한

할수있습니다. 하지만 클래스와 달리 인스턴스는 만들수 없습니다.

이 추상 클래스에서 할수 있는것은 모두 클래스에서 가능한 기능들이지만 왜 굳이 추상 클래스를

사용할까요? 클래스에서 가능하지만 많은 부분을 다른 프로그래머에게 강제할수 없고 설명 또한

필요한 부분이 생길것 입니다. 하지만 추상 클래스는 클래스 자체 만으로 해당 추상 메소드를 강제

할수 있으며 구현되지 않은 부분들은 컴파일러가 알려줄수 있습니다.


- 인터페이스는 명시하지 않은 메서드는 모두 public이 되지만 추상 클래스는 private로 선언됩니다.

- 인터페이스와 달리 직접 구현이 가능하지만 클래스 처럼 인스턴스는 만들수 없습니다.

- 파생 클래스들은 모든 추상 메서드를 구현을 강제 할수 있습니다.

- 추상 클래스가 추상 클래스를 상속한 경우 자식 추상클래스는 부모 추상클래스의 추상 메서드를

  구현하지 않아도 됩니다. (결국 인스턴스를 생성할 클래스에서 구현하기 때문)


소스코드

using System;

namespace ConsoleApp1
{
    //추상클래스 ( 멤버 메서드중 추상 메서드가 하나라도 들어가 있으면 추상 클래스가 됩니다 )
    abstract class Gun
    {
        public int number = 1;

        //발사에 대한 추상적 기능을 강제 할 추상 메서드
        public abstract void Fire();

        //인터페이스와 달리 직접 구현도 가능 합니다.
        public void Reload()
        {
            Console.WriteLine("척커덩!");
        }
    }


    //추상클래스를 상속받은 클래스가 인스턴스화가 가능한 클래스가 되려면 
    //추상함수를 모두 재정의 해야 한다.
    class AutoGun : Gun
    {
        public override void Fire()
        {
            Console.WriteLine("빵야!");
        }
    }

    class LaserGun : Gun
    {
        public override void Fire()
        {
            Console.WriteLine("지이잉~");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Gun g = new AutoGun();
            g.Fire();
            g.Reload();

            if (g is LaserGun)
            {
                LaserGun laser = g as LaserGun;
                if (laser != null)
                    laser.Fire();
            }
            else
            {
                Console.WriteLine("LaserGun 이 없습니다.");
            }
        }
    }
}

결과

빵야!
척커덩!
LaserGun 이 없습니다.




이상으로 인터페이스, 형변환, 추상 클래스의 강좌를 마치도록 하겠습니다.

수고하셨습니다.





1. 구조체 ( struct )


클래스와 닮은면이 많은 구조체는 struct 키워드를 사용하여 선언합니다.

하지만 클래스는 실세계의 객체를 추상화하려는데 그 목적이 있지만 구조체는 데이터만을 위한 데이터 집합체로

목적성이 다릅니다. 또한 클래스와 다르게  은닉성 자체를 강요하지 않으며 편의를 위해 공개적으로 메서드를 

사용하는 경우가 많으며 인스턴스 자체도 스택에 할당되 메서드 호출이 완료되고 메모리에서 사라지게 됩니다.

가비지 컬렉터를 귀찮게 하지 않는 점과 인스턴스 사용이 끝나면 즉시 메모리에서 해제 된다는점 이러한 장점으로

클래스와 비슷하지만 충분히 사용할 가치가 높은 복합 데이터 형식입니다. 


※ 메모리 영역 - 데이터 영역(Data), 힙(Heap), 스텍(Stack)

    *데이터 영역 : static 같은 전역 변수들이 할당되며 프로그램 종료시 해제 됩니다.

    *힙 : class 같은 복합 데이터 형식이 동적으로 메모리에 할당하고 참조자가 끊어지면 가비지 콜렉터에 의해

          자동(해제 시점은 알수 없음)으로 해제 됩니다.

    *스택 : struct와 같은 데이터 형식이 메서드를 호출시 지역변수와 매개변수가 생성되는 곳으로 메서드의

          호출이 끝나면 해제됩니다.  



특징 

클래스 

구조체 

키워드

class

struct

인스턴스 생성

new 키워드로 인스턴스 생성

new 또는 선언 만으로 생성

메모리 영역

힙(Heap)

스텍(Stack)

 복사

얕은 복사

깊은 복사

형식

참조

상속

가능

불가능



선언 형식

struct [구조체 이름]
{
    // 필드
    // 메서드
}


소스 코드

using System;

namespace ConsoleApp1
{
    class Program
    {
       struct TestStruct
       {
            //코드에 명시적으로 초기화 불가능
            //public int number = 10;		
            public int number;

            public void Show()
            {
                Console.WriteLine("Number : {0}", this.number);
            }

            /*
            //구조체는 매개변수가 없는 기본생성자를 재정의 할수 없습니다.
            public TestStruct()
            {
                this.number = 0;
                Console.WriteLine("기본생성자 실행");
            }
            */

            //매개변수가 있는 생성자는 정의 가능 하지만
            //생성자 내에서 모든 맴버 변수들이 초기화 되어야 합니다.
            public TestStruct(int a)
            {
                this.number = a;
            }
        }

        static void Main(string[] args)
        {
            //선언만으로 생성시 지역 변수의 값 할당이 필요
            TestStruct testStruct;
            testStruct.number = 15;
            testStruct.Show();

            //new 만들어진 구조체의 특징
            //new 키워드가 구조체에서 동작하는 방식은 동적 할당되는 개념이 아니고
            //기본 생성자로 초기화 시키는 개념입니다.
            TestStruct testStruct1 = new TestStruct();
            testStruct1.number = 100;
            testStruct1.Show();
            Console.WriteLine();

            //깊은 복사(클론 생성)
            TestStruct testStruct2 = testStruct;
            testStruct2.number = 777;

            testStruct1.Show();
            testStruct2.Show();
            Console.WriteLine();

            //new 키워드로 생성할때 새롭게 정의한 생성자를 선택할 수 있는 기능이 있습니다.
            TestStruct testStruct3 = new TestStruct(-400);      
            testStruct3.Show();
            Console.WriteLine();

            //구조체 배열에 대한 예
            TestStruct[] testStructArr = {
                                             new TestStruct(88),
                                             new TestStruct(20),
                                             new TestStruct(55)
                                         };

            for (int i = 0; i < testStructArr.Length; i++)
            {
                testStructArr[i].Show();
            }
        }
    }
}

결과

Number : 15
Number : 100

Number : 100
Number : 777

Number : -400

Number : 88

Number : 20

Number : 55


소스 코드내에 주석( // )으로 구조체의 기본적인 사용 방법과 특징을 적어 놓았습니다.




2. 오퍼레이터(operator)


오퍼레이터는 정적 멤버 메서드를 정의하여 연산자(+, -, *, /....)를 오버로드할 수 있는 기능입니다.

operator키워드로 선언되며 여러 연산자를 정의 할수 있습니다.


사용 형식

public static [구조체명 반환형식] operator [연산자]([구조체명 매개변수]...)
{
   //...
   return [구조체명 반환형식];
}


연산자 

기호 

오버로드 가능성 

 단항 연산자

 +, -, ~, ++, --, true, false

 가능

 이항 연산자

 +, -, *, /, %, &, |, ^, <<, >>

 가능

 비교 연산자

 ==, !=, <, >, <=, >=

 조건부 가능  비교 연산자는 가능 하지만 다음에

 오는 참고 사항을 참조 )

 조건부 논리 연산자

 &&, ||

 불가능 (오버로드 가능한 & 및 | 를 사용하여 계산 )

 할당 연산자

 +=, -=, *=, /=, %=, &=, |=, ^=, <<=,>>=

 불가능 ( 이항 연산자가 오버로드 되면 

            암시적으로 오버로드 됨)

 기타

 =, ., ?:, ??, ->, =>, f(x), as, new, is ... 등등

 불가능 (키워드)


소스코드

using System;

namespace ConsoleApp1
{
    class Program
    {
        struct Vector2
        {
            public float x;
            public float y;

            public Vector2(float x, float y)
            {
                this.x = x;
                this.y = y;
            }

            //구조체도 object 를 상속받는다.
            public override string ToString()
            {
                //Console.WriteLine() 메서드의 기본 원리는 출력 할수 있는 형식들을
                //ToString() 메서드를 이용하여 문자열로 만들어 출력합니다.
                //새롭게 정의한 구조체 형식을 출력하기 위해 오버로드하여 사용하였습니다.
                return string.Format("{0:F2}, {1:F2}", this.x, this.y);
            }

            //Vector2 끼리 덧셈연산이 가능하게 한다.
            public static Vector2 operator +(Vector2 lhs, Vector2 rhs)
            {
                Vector2 nv = new Vector2();
                nv.x = lhs.x + rhs.x;
                nv.y = lhs.y + rhs.y;

                return nv;
            }
        }

        static void Main(string[] args)
        {
            Vector2 vecPosition = new Vector2(19.2f, 3.5f);
            Console.WriteLine(vecPosition);

            // + 이항연산자가 오버로드 되면 대입 연산자도 암시적으로 오버로드되어
            // += 연산자를 사용할수 있습니다.
            vecPosition += new Vector2(0.0f, 60.0f);	
            Console.WriteLine(vecPosition);
        }
    }
}

결과

19.20, 3.50
19.20, 63.50


구조체는 기본적으로 object를 상속받아 구현되었습니다.

결과 출력을 위해 강좌 8편의 override 기능을 사용해 ToStoring() 메서드를 재정의 하였습니다.

자세한 코드설명은 주석을 참고하여 보시면 되겠습니다.


다음 강좌에서는 인터페이스(Interface), 형변환(is, as), 추상 클래스(abstract) 대해 알아 보도록 하겠습니다.

그럼 수고하셨습니다.






+ Recent posts