using System; public class Parents { public virtual void Say() { Console.WriteLine("Parents"); } //End of virtual Say() } // End of class parents public class Child : Parents { static void Main() { Child say = new Child(); say.Do(); } // End of Main() private void Do() { Parents sayp = new Parents(); sayp.Say(); Child sayc = new Child(); sayc.Say(); } // End of Do() public override void Say() { Console.WriteLine ("Child"); } //End of override Say() } // End of class Child제가 원하는 주석 스타일대로 잘 해주셨네요. 괄호의 끝에 // end of 와 같이 주석을 사용하는 것이 좋습니다. 코드가 길어졌을 때 어디가 클래스의 끝인지, 어디가 메소드의 끝인지 쉽게 알 수 있기 때문입니다. 이것은 개인적인 취향의 차이입니다. 쓰고 싶지 않은 분들은 쓰지 않아도 됩니다. 하지만 C# 처럼 괄호를 많이 쓰는 언어에서는 혼동되지 않게 할 수 있는 좋은 방법이기도 합니다.
public class Child : Parents { public override void Say() { Console.WriteLine ("Child"); } //End of override Say() } // End of class Child class AppMain { static void Main() { Child say = new Child(); say.Do(); } // End of Main() private void Do() { Parents sayp = new Parents(); sayp.Say(); Child sayc = new Child(); sayc.Say(); } // End of Do() }그리고 또 Main에서 Child 클래스를 생성해서 Do 메소드를 호출하고 있고, Do 메소드는 또 Parent와 Child 클래스를 모두 생성하고 있습니다. 이와 같은 형식말고 다음과 같이 Main을 정의해보도록 합시다.
public static void Main() { AppMain ap = new AppMain(); ap.Do(); } // End of Main()이와 같이 한 후, Do 메소드에서 Parent 클래스와 Child 클래스를 각각 테스트합니다. Main을 보면 저는 public 이라고 했습니다. 처음에는 static void Main()과 같이 정의되어 있었습니다. 이것이 의미하는 것은 무엇일까요? private이 기본이라는 것을 나타내지만 이것은 개발자가 하고 있는 하나의 가정(C#에서는 기본 형식이 private이라는 것을 알고 있어야 한다는 가정)입니다. 이러한 가정은 다른 언어를 사용하는 프로그래머들은 모르기 때문에 다른 프로그래머의 작업을 어렵게 할 겁니다. ^^;
public class Parents { public Parents() { Console.WriteLine("Parent Constructor"); } public virtual void Say() { Console.WriteLine ("Parent"); } //End of virtual Say() } // End of class Parents이와 같이 하고 다시 컴파일해서 실행해 봅니다. 이번에는 실행 결과가 Parent Constructor, Parent, Child가 차례대로 출력되는 것을 알 수 있습니다. Parent Constructor는 어디서 출력될까요?
Parents sayp = new Parents();위 문장에서 첫번째 Parents는 클래스 이름을 지칭합니다. sayp는 클래스의 인스턴스에서 사용할 이름을 지정하는 것입니다. new는 새로운 클래스를 만들라는 의미입니다(붕어빵 기계에서 붕어빵을 찍어내라는 이야기). Parents()는 뭘까요? 클래스 이름이 아니라 생성자를 지칭하는 것입니다. 여기서는 Parents()라는 메소드를 사용해서 클래스에 대한 인스턴스를 생성하라는 의미입니다.
public class Parents { public Parents() { Console.WriteLine("Parent Constructor"); } public Parents(string str) { Console.WriteLine("Parent Constructor: " + str ); } public virtual void Say() { Console.WriteLine ("Parent"); } //End of virtual Say() } // End of class Parents AppMain 클래스의 Do 메소드를 다음과 같이 변경합니다. private void Do() { Parents sayp = new Parents("Vernon"); sayp.Say(); Child sayc = new Child(); sayc.Say(); } // End of Do()소스 코드를 다시 컴파일하고 실행하면 결과가 달라집니다. 두 번째 생성자가 호출됩니다. 컴파일러는 Parents sayp = new Parents("Vernon");을 만나는 순간에 Parents 클래스의 생성자들 중에 어떤 것을 사용해야 할지 알 수 있습니다. 왜 알 수 있죠? 제가 처음에 설명했습니다. 메소드 이름과 메소드에 전달되는 인자들을 모두 합쳐서 메소드 서명이라고 한다고 했습니다. 이 메소드 서명을 통해서 컴파일러는 어떤 것을 사용하려고 하는지 알아낼 수 있습니다. 쉽게 말해서 뭐죠? 서명이 일치하는 것을 찾아내는 겁니다.
public class Child : Parents { public Child() { Console.WriteLine("Child Constructor"); } public override void Say() { Console.WriteLine ("Child"); } //End of override Say() } // End of class Child예제를 다시 컴파일하고 실행하면 조금 의외의 결과가 나타나는 것을 볼 수 있습니다. Child 클래스에 대한 인스턴스를 생성하는 순간 Parents 클래스의 생성자로 함께 실행되는 것을 알 수 있습니다. 왜 그렇죠? Child 클래스가 Parents 클래스를 상속받기 때문에 그렇습니다. 지금까지 우리는 생성자를 만들어쓰지 않았습니다. 그런데 어떻게 Class aClass = new Class(); 와 같이 생성자를 지정할 수 있었을까요? 이런 경우에 C# 컴파일러가 생성자에 대한 껍데기를 자동으로 만들기 때문에 그렇습니다.
using System; public class Additem { /******************************************* * * int 형과 int 형을 더하는 메소드 * *******************************************/ public void Add(int val1,int val2) { int sum = val1 + val2; Console.WriteLine("Result =" + sum.ToString() ); } // End of Add(int) /******************************************** * * float형과 float형을 더하는 메소드 * ********************************************/ public void Add(float fval1, float fval2) { float sum = fval1 + fval2; Console.WriteLine("Result =" + sum.ToString()); } // End of Add(float) } // End of Additem public class AppMain { static void Main() { AppMain ap = new AppMain(); ap.AddData(); } // End of Main() private void AddData() { Additem a1 = new Additem(); a1.Add(val1,val2); Additem a2 = new Additem(); a2.Add(fval1,fval2); } // End of AddData private int val1 = 1; private int val2 = 2; private float fval1 = 3f; private float fval2 = 4f; } // End of class AppMain와우! 이 분의 코드는 잘 실행됩니다. 주석도 꽤 공들여서 했습니다. 제가 늘 하는 말이 있습니다. 아름다운 코드가 보기에도 좋다! 다른 말이 또 있죠. 보기 좋은 떡이 먹기도 좋다! 뭐, 또 아리따운 아가씨도 있으면 좋겠지만, 그런 경우에는 남자들한테 흔히 하는 말이 있죠. 견물생심, 그림에 떡이라고…(웃음) 먼저 오버로딩이 어디서 어떻게 되어 있는지 살펴보도록 하지요.
public void Add(int val1,int val2) public void Add(float fval1, float fval2)위 부분이 오버로딩을 구현한 부분입니다. 네, 이처럼 메소드 서명이 다른 경우에 오버로딩이 일어납니다. 그리고 Add(1, 3)과 Add(1.1f, 4.5f)를 만나게 될 때 컴파일러는 서명이 일치하는 것을 찾아서 실행하게 되는 것이지요.
private void AddData() { Additem a1 = new Additem(); a1.Add(val1,val2); Additem a2 = new Additem(); a2.Add(fval1,fval2); } // End of AddData잘 작성되어 있습니다. 하지만 꼭 두 개의 인스턴스를 생성할 필요는 없습니다. 오버로딩의 의미가 다른 Add를 갖는 각각의 인스턴스를 자동으로 만들어 준다든가 하는 것은 아니니까요. 그저 a1.Add(val1, val2);와 a1.Add(fval1, fval2); 와 같이 사용하면 됩니다. 또한 a1.Add(val1,val2);와 같이 몽땅 붙여서 쓰지 마세요. 제가 쓴 것처럼 모두 띄어 쓰세요. 왜 이렇게 할까요?
private int val1 = 1; private int val2 = 2; private float fval1 = 3f; private float fval2 = 4f;AddData() 메소드 밑에 위와 같이 내부 데이터가 있습니다. 이 경우에 AddData() 메소드에서 테스트하는 데이터가 별도로 떨어져 있습니다. 만약 AddData() 메소드와 테스트에 사용할 데이터들 사이에 코드가 너무 많이 있어서 서로 떨어져 있다면 이러한 값들이 어디서 오는지 알기 어렵게 됩니다. 그런 경우도 있지 않나요? 다른 사람이 작성한 코드를 보다 보면 ii, jj, kk라는 변수에 도대체 어떤 값이 들어가고, 어떤 역할을 하는지 몰라서…
private void AddData() { int val1 = 1; int val2 = 2; float fval1 = 3f; float fval2 = 4f; Additem a1 = new Additem(); a1.Add(val1, val2); a1.Add(fval1, fval2); } // End of AddData이것으로 제대로 된 코드가 완성된 것 같습니다. 여기서는 this.val1과 같이 사용하지 않습니다. 클래스의 변수가 아니라 메소드 내부에 선언한 변수를 사용하는 것이니까요. ^^; 여러분이 작성한 코드가 제대로 동작하지 않는다는 의미가 아닙니다. 보다 객체 지향에 맞게, 또는 보다 유지 보수하기 쉽고, 프로그래밍하기 쉬운 코드가 되었다는 의미입니다.
int a = 5; int b = 7; Console.WriteLine( (a + b).ToString() );이와 같이 얘기했습니다. 여기서 +는 숫자와 숫자를 더하는 연산 기호지요. 연산 기호에는 4가지가 있다고 했습니다. 뭐가 있다고 했지요? +, -, *, /이 있다고 했습니다. 그리고 이중에서 +만 가장 많이 사용한다고 했습니다. 나머지는 자주 사용하지는 않습니다. 어쨌거나 만약에 a * b를 계산한다고 하지요. 여기서는 값을 미리 정했지만 계산기 프로그램을 작성한다면 사용자가 어떤 값을 넣는지 알 수 없지요?
try { // 에러가 발생할 지도 모르는 코드 }이와 같이 에러가 발생할 수 있는 코드를 try로 감쌉니다. 그리고 에러가 발생하면 발생한 에러를 잡기 위해 catch 문을 추가합니다. catch문을 추가하면 다음과 같습니다.
try { // 에러가 발생할 지도 모르는 코드 } catch( Exception e ) { Console.WriteLine(e.Message.ToString()); }그리고 예외가 발생하는 것과 상관없이 항상 실행해야 할 코드들을 지정하기 위해 finally 블록을 사용합니다. 예를 들어 게임 프로그래밍을 하고 있는데 알 수 없는 에러가 발생했다고 "검은 화면"만 보여주고 프로그램이 죽어버려서는 안되겠지요. 어떻게든 게임 프로그램을 끝내고 원래 윈도우 화면으로 돌아갈 수 있게 해주어야 합니다. 이럴때 finally가 위력을 발휘합니다.
try { // 에러가 발생할 지도 모르는 코드 } catch( Exception e ) { Console.WriteLine(e.Message.ToString()); } finally { // 반드시 실행해야 할 코드 }이와 같이 하여 보다 안전하게 예외를 잡을 수 있습니다. 예외에는 이 외에도 많은 유형들이 있습니다. 그중에 대표적인 것으로는 OverflowExceptionsk IndexOutOfRangeException 등이 있습니다. 이름에서 알 수 있는 것처럼 int 형이 담을 수 있는 데이터 크기를 넘어버리면 OverflowException이 발생합니다. 위에 있는 Exception은 모든 예외를 잡아버릴 수 있는 "만병통치약"입니다. ㅎㅎ…
try { // 에러가 발생할 지도 모르는 코드 } catch( OverflowException oe ) { Console.WriteLine(e.Message.ToString()); } catch( IndexOutOfRangeException rangeException ) { Console.WriteLine(rangeException.Message.ToString()); } catch( Exception e ) { Console.WriteLine(e.Message.ToString()); } finally { // 반드시 실행해야 할 코드 }이처럼 catch는 여러 개를 사용할 수 있습니다. 그리고 이러한 "약"들이 모두 듣지 않을 경우, 가장 마지막으로 정체가 뭔지 모를 "만병통치약" Exception을 사용하는 겁니다.
System.Exception - System.OverflowException - System.SystemException - System.IndexOutOfRangeException - System.OutOfMemoryException여기서 알 수 있는 것처럼 Exception 클래스도 사실은 복잡한 계층 구조로 되어 있는 것을 알 수 있습니다. SystemException은 Exception 클래스를 상속하고, IndexOutOfRangeException이나 OutOfMemoryException은 System.Exception 클래스를 상속하고 있는 것을 알 수 있습니다.
이전 글 : PC 되살리기
최신 콘텐츠