[Flags]는 특별한 목적을 위해 사용되는 특성(attribute)이다. 주로 enum 형식을 비트 연산을 위한 플래그 집합으로 사용할 때 붙인다. 이를 통해 여러 가지 상태를 한 변수에 저장하고, 비트 단위의 연산을 수행할 수 있다.
< [Flags]의 정의와 사용 시기 >
[Flags] 속성은 enum(열거형)에 대해 선언되며, 각각의 값이 비트 수준에서 조합될 수 있음을 나타낸다. 이는 주로 '플래그(flag)' 역할을 하는 enum을 정의할 때 사용된다. 이렇게 선언된 enum 값은 비트 OR 연산자(|)를 이용해 합쳐지거나, 비트 AND 연산자(&)를 통해 특정 플래그가 활성화되었는지 확인할 수 있다.
각 플래그는 2의 거듭제곱을 사용하므로, 각 플래그가 고유한 비트를 차지한다. 이렇게 설정함으로써 각 플래그를 조합하여 사용할 수 있다. [[Flags]의 사용 예]
MyFlags activeFlags = MyFlags.Flag1 | MyFlags.Flag3;
// Flag1 and Flag3 are active
Console.WriteLine(activeFlags.HasFlag(MyFlags.Flag1)); // prints True
Console.WriteLine(activeFlags.HasFlag(MyFlags.Flag3)); // prints True
// Flag2 and Flag4 are not active
Console.WriteLine(activeFlags.HasFlag(MyFlags.Flag2)); // prints False
Console.WriteLine(activeFlags.HasFlag(MyFlags.Flag4)); // prints False
위 코드에서는 MyFlags.Flag1와 MyFlags.Flag3를 OR 연산을 통해 activeFlags에 저장한다. 이후 HasFlag 메소드를 통해 각 플래그가 활성화되었는지를 확인할 수 있다.
< [Flags] 사용 시 주의사항 >
[Flags]를 사용할 때는 두 가지 주요한 주의사항이 있다. 플래그의 값이 2의 거듭제곱이어야 한다. 이렇게 설정해야 각 플래그가 고유한 비트를 차지할 수 있다. None 값은 항상 0을 사용해야 한다. 이는 아무런 플래그도 설정되지 않았음을 나타내는 값으로, 해당 플래그를 모두 클리어하는데 사용된다.
프로그래밍에서 데이터 보호는 중요한 이슈 중 하나다. 그래서 다양한 데이터 접근 제어 방법을 사용하여 데이터를 보호한다. C#에서도 이를 위해 접근 제어자(access modifiers)를 제공하고 있다. 특히 'public'과 'protected' 속성 그리고 'private' 멤버에 대해 깊게 살펴볼 것이다.
< 데이터 접근 제어란? >
데이터 접근 제어는 클래스의 멤버에 대한 접근을 제어하는 방식을 의미한다. 멤버는 필드, 메소드, 프로퍼티 등을 포함한다. C#에서는 접근 제어자를 사용해 이를 수행할 수 있으며, 가장 일반적으로 사용되는 접근 제어자로는 public, private, protected, internal 등이 있다.
[Public과 Protected 속성]
C#에서 속성(Property)은 객체의 상태를 표현하는 데 사용된다. 이를 통해 객체의 내부 상태를 안전하게 읽고 수정할 수 있다. 속성은 필드처럼 보이지만, 내부적으로는 메서드인 Get과 Set을 통해 동작한다.
Public 속성은 클래스의 외부에서 자유롭게 접근할 수 있도록 하는 방식이다. 즉, 클래스가 속한 어셈블리나 다른 어셈블리에서도 이 속성에 접근할 수 있다.
public class MyClass {
public string MyProperty { get; set; }
}
반면에, Protected 속성은 해당 클래스와 해당 클래스를 상속받은 클래스에서만 접근 가능하게 한다. 그래서 다른 클래스에서는 직접적인 접근이 불가능하다.
public class MyBaseClass {
protected string MyProperty { get; set; }
}
Public과 Protected 속성은 클래스의 사용자에게 데이터에 직접적으로 접근할 수 있는 방법을 제공한다. 하지만 이는 데이터의 상태를 제어하는 측면에서 위험성을 내포하고 있다.
< 모든 데이터 멤버는 Private으로 >
이런 위험성을 피하기 위해, 클래스의 모든 데이터 멤버는 가능한 Private으로 선언하는 것이 바람직하다. Private 멤버는 오직 그 멤버가 선언된 클래스에서만 접근 가능하며, 외부에서는 접근할 수 없다.
public class MyClass {
private int myField;
}
이 방법은 데이터 캡슐화를 강화하며, 클래스 내부의 상태를 외부로부터 보호하는데 중요한 역할을 한다. 이는 클래스를 수정하거나 확장할 때도 이점을 준다. 외부에 노출된 부분이 적을수록 클래스의 구현을 수정하거나 확장하기 쉽기 때문이다.
< 인덱서(Indexer)의 사용 >
마지막으로, C#에서는 배열이나 컬렉션과 같은 시퀀스나 딕셔너리에 접근할 때 인덱서(Indexer)를 사용하는 것이 좋다. 인덱서는 클래스나 구조체의 인스턴스를 배열처럼 접근할 수 있게 해주는 멤버다.
public class MyClass {
private List<string> myList = new List<string>();
public string this[int index] {
get { return myList[index]; }
set { myList[index] = value; }
}
}
이렇게 하면, 클래스의 사용자는 내부 데이터를 배열처럼 다룰 수 있게 된다. 하지만 실제로 데이터가 어떻게 저장되고 접근되는지는 완전히 감추어진다. 이로 인해 데이터의 캡슐화가 더욱 강화된다.
이렇게 public이나 protected로 데이터를 노출할 때는 항상 속성을 사용하고, 시퀀스나 딕셔너리를 노출할 때는 인덱서를 사용하며, 모든 데이터 멤버는 private으로 선언하게 되면 클래스의 캡슐화를 강화하고 더욱 안전하고 유지보수하기 쉬운 코드를 작성할 수 있다.
인덱서를 사용하면 내부 데이터에 직접 접근하는 것이 아니라 메소드를 통해 접근하므로 유효성 검사나 추가적인 계산을 수행할 수 있다. 예를 들어, 내부 배열에 접근하는 인덱서에 유효한 인덱스만 허용하도록 유효성 검사를 추가할 수 있다.
public T this[int i]
{
get
{
if (i >= 0 && i < arr.Length)
{
return arr[i];
}
else
{
throw new IndexOutOfRangeException();
}
}
set
{
if (i >= 0 && i < arr.Length)
{
arr[i] = value;
}
else
{
throw new IndexOutOfRangeException();
}
}
}
이 코드는 인덱스가 배열의 범위를 벗어나지 않는지 확인하고, 그렇지 않은 경우 IndexOutOfRangeException을 발생시킨다.
< 인덱서와 상속 >
인덱서는 virtual, abstract 또는 override 키워드와 함께 사용될 수 있다. 이를 통해 인덱서의 동작을 하위 클래스에서 변경하거나 확장할 수 있다. 예를 들어, 기본 클래스에서 virtual 키워드를 사용해 인덱서를 정의하고, 하위 클래스에서 override 키워드를 사용해 그 동작을 변경할 수 있다.
public virtual T this[int i]
{
get { /* 기본 구현 */ }
set { /* 기본 구현 */ }
}
// 하위 클래스에서
public override T this[int i]
{
get { /* 변경된 구현 */ }
set { /* 변경된 구현 */ }
}
이를 통해 클래스 계층에서 인덱서의 동작을 더욱 유연하게 관리할 수 있다.
< 인터페이스와 인덱서 >
인덱서는 인터페이스에도 포함될 수 있다. 이를 통해 특정 클래스가 반드시 구현해야 하는 인덱서를 지정할 수 있다.
public interface ISampleCollection<T>
{
T this[int i] { get; set; }
}
// 이 인터페이스를 구현하는 클래스에서
public class SampleCollection<T> : ISampleCollection<T>
{
// 인터페이스의 인덱서 구현
public T this[int i]
{
get { /* 구현 */ }
set { /* 구현 */ }
}
}
이를 통해 클래스 간의 계약을 정의하고 코드의 일관성을 유지할 수 있다.
< 읽기 전용 또는 읽기/쓰기용 인덱서 >
인덱서는 get 접근자만을 포함하여 읽기 전용으로 만들 수 있다. 반대로 get과 set 접근자를 모두 포함하여 읽기/쓰기용으로 만들 수도 있다. 이를 통해 클래스의 내부 데이터에 대한 접근을 더욱 세밀하게 제어할 수 있다.
// 읽기 전용 인덱서
public T this[int i]
{
get { /* 구현 */ }
}
// 읽기/쓰기용 인덱서
public T this[int i]
{
get { /* 구현 */ }
set { /* 구현 */ }
}
이를 통해 클래스의 내부 데이터를 보호하고 외부에서의 변경을 제어할 수 있다.
< 데이터 바인딩과 인덱서 >
정수를 매개변수로 받는 1차원 인덱서는 데이터 바인딩에 유용하게 사용될 수 있다. 데이터 바인딩이란 데이터를 UI 요소에 연결하는 프로세스를 의미한다. 이런 방식으로, 인덱서를 통해 각 항목에 접근하면서 데이터를 UI 요소에 연결할 수 있다.
< 맵 정의와 인덱서 >
또한, 정수 이외의 매개 변수를 받는 인덱서는 맵(Map)을 정의하는데 유용하다. 키-값 쌍을 저장하는 자료구조인 맵에 접근하기 위해 인덱서를 사용할 수 있다. 예를 들어, 문자열 키와 값을 갖는 맵을 정의하려면 다음과 같이 인덱서를 사용할 수 있다.
public T this[string key]
{
get { /* key에 해당하는 값을 반환 */ }
set { /* key에 해당하는 값을 설정 */ }
}
이렇게 해서 인덱서는 클래스가 배열처럼 동작하게 하고, 동시에 캡슐화를 유지하면서 복잡한 동작을 가능하게 할 수 있다.
클래스 내에 데이터 필드를 선언하고 이를 접근하기 위한 메서드를 작성하는 것은 매우 일반적인 일이다. 이렇게 필드에 대한 접근 제어를 하기 위해 getter와 setter를 사용한다. 이렇게 필드와 이를 제어하는 메서드를 함께 묶어 속성(Property)이라 부른다.
public class Coffee
{
private string _name; // 필드
public string Name // 속성
{
get { return _name; } // getter
set { _name = value; } // setter
}
}
그러나 이와 같은 방식은 코드의 중복성을 증가시키고 가독성을 낮추는 문제가 있다. 그래서 이를 해결하기 위해 자동속성(Auto-Implemented Properties)이라는 개념을 도입했다.
public class Coffee
{
public string Name { get; set; }
}
자동속성을 사용하면 필드는 컴파일러에 의해 자동으로 생성되며, getter와 setter 메서드도 자동으로 생성된다. 이는 코드의 중복성을 줄이고 가독성을 높여 준다.
자동속성은 특히 데이터 전송 객체(Data Transfer Object, DTO)나 데이터 모델 클래스 등의 작성에 주로 사용된다. 이들 클래스는 데이터를 저장하고 전달하는 역할을 하는데, 이때 개별 필드에 대해 별도의 로직 없이 단순히 데이터를 저장하거나 반환하는 작업이 주로 이루어지기 때문에 자동속성을 사용하면 매우 효율적이다.
[주의사항] 자동속성은 필드에 대해 별도의 로직이 없을 때 사용하는 것이 좋다. 필드의 값을 가져오거나 설정하는 과정에서 별도의 검증 로직이나 계산이 필요한 경우에는 일반 속성을 사용하는 것이 좋다. 자동속성은 기본적으로 public으로 선언되지만, 필요에 따라 접근 제어자를 변경할 수 있다. 하지만 get과 set에 대해 별도의 접근 제어자를 지정할 수도 있으니 이 점을 주의해야 한다. 예를 들어, 다음과 같이 속성을 선언할 수 있다.
public class Coffee
{
public string Name { get; private set; } // 외부에서는 읽기만 가능하고, 클래스 내에서만 값 설정 가능
}
< 가상속성(Virtual Properties) >
가상속성은 상속받은 클래스에서 재정의(Override)할 수 있는 속성이다. 이는 다형성(Polymorphism)의 한 형태로, 같은 이름의 속성이지만 각각의 클래스에서 다른 동작을 수행하도록 하는 것이 가능해진다.
public class Vehicle
{
public virtual string Name { get; set; } = "Vehicle";
}
public class Car : Vehicle
{
public override string Name { get; set; } = "Car";
}
이 경우, Vehicle 객체의 Name 속성은 "Vehicle"을 반환하고, Car 객체의 Name 속성은 "Car"을 반환한다. 가상속성은 이와 같이 상위 클래스에서 정의한 속성을 하위 클래스에서 필요에 따라 다르게 동작하도록 할 때 사용한다.
[주의사항] 하위 클래스에서 속성을 재정의할 때는 반드시 override 키워드를 사용해야 한다. virtual 키워드를 사용하면 해당 속성이나 메서드는 재정의될 수 있음을 의미한다. 따라서 virtual 키워드를 사용할 때는 하위 클래스에서 이를 재정의할 가능성을 염두에 두고 설계해야 한다. virtual 키워드를 사용할 경우, 실행 시간에 적절한 메서드나 속성이 호출되기 때문에 약간의 성능 손실이 발생할 수 있다. 따라서 성능이 중요한 상황에서는 이를 고려해야 한다.
차의 운전석에 앉아, 차를 운전할 때, 만약 핸들을 돌린 방향과 차량이 반대 방향으로 움직인다면 어떻게 될까? 위험하고 혼란스러울 것이다. 이처럼, 행동과 그 결과가 일관되게 반응하는 것이 중요한데 이와 비슷하게, 사용자 인터페이스(UI)를 다룰 때 우리는 UI 요소와 데이터 간의 일관성을 유지하려고 노력한다. 여기서 C#의 DataBindings 속성이 활용되는데, 이것은 UI 컴포넌트와 데이터 소스를 "연결"해주는 역할을 한다.
< 데이터 바인딩이란 무엇인가? >
"데이터 바인딩(Data Binding)"이란 이름에서 알 수 있듯이, 데이터와 어떤 요소를 '연결'하는 것이다. 사용자 인터페이스 요소 (예: 텍스트 박스)와 데이터 소스(예: 객체의 속성) 간에 '연결고리'를 만들어준다. 이로 인해 데이터가 변경될 때 UI가 자동으로 업데이트되고, 반대로 UI에서 사용자가 변경을 가하면 데이터 소스에도 반영된다.
왜 데이터 바인딩을 사용할까?
[일관성 유지] 데이터 바인딩의 가장 큰 장점 중 하나는 UI와 데이터 사이의 일관성을 유지할 수 있다는 것이다. 예를 들어, 데이터의 값이 변경되면 UI에 해당 변경이 자동으로 반영되어 사용자가 최신 상태를 보게 된다.
[코드 양 감소] 데이터 바인딩을 사용하면 UI 업데이트를 위한 코드를 수동으로 작성할 필요가 없어진다. 데이터의 변경을 감지하고 UI를 적절하게 업데이트하는 것을 프레임워크가 알아서 처리해준다.
[유지보수 용이] 데이터 바인딩은 모델과 뷰 간의 강력한 결합을 준다. 따라서 코드의 유지보수가 더 쉬워지고, 각 부분을 독립적으로 테스트하거나 변경하는 것이 용이해진다.
언제 데이터 바인딩을 사용하는가?
UI에서 표시하는 데이터가 자주 변경되거나, 사용자의 입력을 통해 데이터가 변경되는 경우 데이터 바인딩을 사용하면 매우 효율적이다. 또한, 복잡한 사용자 인터페이스를 가지고 있고 그 인터페이스가 다양한 데이터 소스와 상호작용하는 경우에도 유용하다.
여기서 textBoxCity는 사용자가 도시 이름을 입력할 수 있는 텍스트 박스다. address는 주소 정보를 담고 있는 객체이고, City는 그 중 도시 이름을 나타내는 속성이다.
이 코드를 통해 텍스트 박스의 'Text' 속성과 address 객체의 'City' 속성이 연결된다. 이제 사용자가 텍스트 박스에 도시 이름을 입력하면 address 객체의 'City' 속성이 자동으로 업데이트된다. 반대로, address 객체의 'City' 속성이 프로그램에 의해 변경되면 그 변경 사항이 텍스트 박스에 자동으로 반영된다.
Address.cs를 아래와 같이 만들고
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WindowsFormsApp2
{
public class Address : INotifyPropertyChanged
{
private string city;
public event PropertyChangedEventHandler PropertyChanged;
public string City
{
get { return city; }
set
{
if (city != value)
{
city = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(City)));
}
}
}
}
}
아래와 같이 Form1 코드를 작성한다 여기서 TextBox는 도구 툴에서 만들어도 된다.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp2
{
public partial class Form1 : Form
{
private Address address;
// textBoxCity를 선언
private TextBox textBoxCity = new TextBox();
public Form1()
{
InitializeComponent();
// Address 객체를 생성하고 초기 도시를 '서울'로 설정
address = new Address() { City = "서울" };
// textBoxCity의 Text 속성을 address 객체의 City 속성에 바인딩
textBoxCity.DataBindings.Add("Text", address, nameof(Address.City), false, DataSourceUpdateMode.OnPropertyChanged);
// textBoxCity를 폼에 추가 가능 .
this.Controls.Add(textBoxCity);
}
private void Form1_Load(object sender, EventArgs e)
{
}
}
}
Dictionary는 키와 값 쌍을 저장하는 컬렉션이다. 다른 언어에서는 '해시맵', '해시 테이블', '맵' 등으로 불리는 자료구조와 같다. Dictionary는 키를 사용하여 빠르게 데이터를 검색할 수 있도록 설계되어 있다.
Dictionary<TKey, TValue>는 두 개의 타입 매개변수를 사용한다
TKey: 딕셔너리의 키의 타입 지정
TValue: 딕셔너리의 값의 타입을 지정
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 정수 키와 문자열 값을 저장하는 Dictionary 생성
Dictionary<int, string> dictionary = new Dictionary<int, string>();
// 키-값 쌍 추가
dictionary.Add(1, "One");
dictionary.Add(2, "Two");
dictionary.Add(3, "Three");
// 키를 사용하여 값을 검색
string value = dictionary[2];
Console.WriteLine(value); // 출력: Two
// 키-값 쌍 제거
dictionary.Remove(1);
// Dictionary의 모든 키-값 쌍을 반복 처리
foreach (var pair in dictionary)
{
Console.WriteLine($"Key: {pair.Key}, Value: {pair.Value}");
}
// 키가 Dictionary에 존재하는지 확인
if (dictionary.ContainsKey(3))
{
Console.WriteLine("Key 3 exists in the dictionary.");
}
}
}
① 하나의 배열이 주어지고, 해당 배열의 각 데이터는 숫자다. 배열 절반의 길이보다 큰 갯수만큼 특정 숫자가 들어가 있으면 해당 숫자,그렇지 않으면 -1을 리턴하는 함수를 만들어라.
예: [2,7,7,7,1,7,2], 리턴값 : 7
using System;
using System.Collections.Generic;
class Program
{
static int FindMajorityElement(int[] arr)
{
// 숫자와 빈도수를 저장하는 Dictionary 생성
Dictionary<int, int> counts = new Dictionary<int, int>();
// 각 숫자의 빈도수를 계산
foreach (int number in arr)
{
if (counts.ContainsKey(number))
{
counts[number]++;
}
else
{
counts[number] = 1;
}
}
// 배열의 길이의 절반을 계산
int halfLength = arr.Length / 2;
// 빈도수가 배열 길이의 절반보다 큰 숫자를 찾음
foreach (var entry in counts)
{
if (entry.Value > halfLength)
{
return entry.Key;
}
}
// 해당하는 숫자가 없는 경우 -1을 리턴
return -1;
}
static void Main()
{
// 예제
int[] arr = {2, 7, 7, 7, 1, 7, 2};
int result = FindMajorityElement(arr);
Console.WriteLine("리턴값: " + result); // 리턴값: 7
}
}
② 최빈값 찾기: 주어진 배열에서 가장 많이 등장하는 숫자를 찾아서 반환하라. 만약 두 개 이상의 숫자가 같은 횟수로 등장한다면, 그 중 아무 숫자나 반환하라.
using System;
using System.Collections.Generic;
class Program
{
static int FindMode(int[] arr)
{
if (arr == null || arr.Length == 0)
{
throw new ArgumentException("Input array should not be null or empty");
}
Dictionary<int, int> counts = new Dictionary<int, int>();
// 배열의 모든 요소에 대해 반복하여 빈도수를 계산한다.
foreach (int number in arr)
{
if (counts.ContainsKey(number))
{
counts[number]++;
}
else
{
counts[number] = 1;
}
}
int maxCount = 0; // 최대 빈도수를 저장할 변수.
int mode = 0; // 최빈값을 저장할 변수.
// Dictionary의 모든 항목에 대해 반복한다.
foreach (var entry in counts)
{
if (entry.Value > maxCount) // 현재 항목의 빈도수가 최대 빈도수보다 크면
{
maxCount = entry.Value; // 최대 빈도수를 현재 항목의 빈도수로 갱신한다.
mode = entry.Key; // 최빈값을 현재 숫자로 설정한다.
}
}
return mode; // 최빈값을 반환한다.
}
static void Main()
{
int[] arr = { 4, 1, 2, 2, 3, 3, 4 };
try
{
int mode = FindMode(arr); // FindMode 메서드를 호출하여 최빈값을 얻는다.
Console.WriteLine("Mode: " + mode); // 최빈값을 콘솔에 출력한다.
}
catch (ArgumentException e) // ArgumentException이 발생하면
{
Console.WriteLine(e.Message); // 예외 메시지를 콘솔에 출력한다.
}
}
}