확장 메서드는 C# 3.0부터 추가되었고, 대표적인 예로는 Linq가 있다. Linq의 대부분의(거의 모든) 메서드는 IEnumerable<T> 인터페이스의 확장 메서드로 구현되어 있다.
1. 구현
기본적인 구현방법은 class를 public static으로 선언하고, 메서드 역시 public static으로 선언하며, 확장하고자 하는 class Type 앞에 this 키워드를 붙여서서 선언한다. 구현부는 일반 메서드와 같다.
아래는 String에서 단어의 갯수를 반환해주는 간단한 예제다.
using System; namespace ExtensionMethods { public static class MyExtensions { public static int WordCount(this String str) { return str.Split(new char[] { '' '', ''.'', ''?'' }, StringSplitOptions.RemoveEmptyEntries).Length; } } }
2. 사용
확장 메서드의 사용방법은 확장 메서드가 구현된 class의 Namespace를 using 절에 추가하여, 일반 멤버 메서드와 동일하게 사용가능하다. 해당 class의 intellisence에서는 (Extension) 이라는 키워드가 붙는거 이외엔 멤버 메서드와 동일하다. 아래는 위에서 구현한 확장 메서드의 사용 예제이다.
using System; using ExtensionMethods; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { string s = "Hello Extension Methods"; int i = s.WordCount(); Console.WriteLine("WordCount = {0}", i); } } }
3. 주의사항
확장할 class의 멤버 메서드에 동일한 구조의 메서드가 있을 경우 호출 되지 않는다. 또한 Namespace 범위로 확장 메서드를 가져오게 되므로 호출하는 코드에서는 반드시 확장 메서드가 구현된 class의 Namespace를 using 절에 추가하여야 한다.
Microsoft C# 프로그래밍 가이드에서는 반드시 필요한 경우가 아니면 확장 메서드 대신에 class의 상속으로 구현하라고 가이드한다. 기존 class에 확장 메서드와 동일한 구조의 멤버 메서드가 추가되면 확장 메서드가 호출되지 않고 멤버 메서드가 호출되기 때문이다. 예제는 링크를 참조하기 바란다.
4. 의견
나는 주로 기존 클래스의 구조를 무너뜨리지 않으면서 필요한 기능을 확장하고 싶을때 사용한다. Util관련 메서드들을 확장 메서드로 구현하곤 한다. Namespace를 추가하면 간단하게 사용가능하기 때문이다. class의 상속도 하나의 방법이지만, 상속된 class가 증가함에 따라 각 class간의 결합도는 증가하기 때문에 되도록 상속을 피하고, 확장 메서드가 포함된 class를 별도의 라이브러리로 관리하여 코드의 재사용율을 높이는 편이다.
아래는 내가 자주사용하는 확장 메서드의 구현이다. 필요할 때마다 여러 프로젝트에 사용하고자 계속 업데이트할 계획이다.
namespace System { public static class StringExtensions { } public static class ArrayExtensions { public static T[] CopySlice<T>(this T[] source, int index, int length, bool padToLength = false) { int n = length; T[] slice = null; if (source.Length < index + length) { n = source.Length - index; if (padToLength) { slice = new T[length]; } } if (slice == null) slice = new T[n]; Array.Copy(source, index, slice, 0, n); return slice; } public static IEnumerable<T[]> Slices<T>(this T[] source, int count, bool padToLength = false) { for (var i = 0; i < source.Length; i += count) yield return source.CopySlice(i, count, padToLength); } } public static class ByteArrayExtensions { /// <summary> /// 각 byte를 Hex를 나타내는 문자열로 반환합니다. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="bytes"></param> /// <returns></returns> public static string ToHexString(this IEnumerable<byte> bytes) { var hexString = new StringBuilder(); foreach (var byteFromHash in bytes) { hexString.AppendFormat("{0:x2}", byteFromHash); } return hexString.ToString(); } } public static class DataRowExtensions { /// <summary> /// DbNull일 경우 defaultValue, 아닐 경우 컬럼의 값을 가져온다. /// SQLite의 INTEGER 타입의 경우 반드시 Int64로 가져와야 한다. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dataRow"></param> /// <param name="columnName"></param> /// <param name="defaultValue"></param> /// <returns></returns> public static T GetFieldValue<T>(this DataRow dataRow, string columnName, T defaultValue) { if (dataRow.Table.Columns.Contains(columnName) != true || dataRow.IsNull(columnName)) { return defaultValue; } else { return dataRow.Field<T>(columnName); } } /// <summary> /// DbNull일 경우 T의 기본값, 아닐 경우 컬럼의 값을 가져온다. /// SQLite의 INTEGER 타입의 경우 반드시 Int64로 가져와야 한다. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="dataRow"></param> /// <param name="columnName"></param> /// <returns></returns> public static T GetFieldValue<T>(this DataRow dataRow, string columnName) { return dataRow.GetFieldValue<T>(columnName, default(T)); } public static T Cast<T>(this DataRow tableRow) where T : new() { Type t = typeof(T); T returnObject = new T(); foreach (DataColumn col in tableRow.Table.Columns) { string colName = col.ColumnName; PropertyInfo pInfo = t.GetProperty(colName.ToLower(), BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (pInfo == null) continue; MethodInfo mInfo = pInfo.GetSetMethod(); if (pInfo != null && mInfo != null) { object val = tableRow[colName]; bool IsNullable = (Nullable.GetUnderlyingType(pInfo.PropertyType) != null); if (IsNullable) { if (val is System.DBNull) { val = null; } else { val = Convert.ChangeType(val, Nullable.GetUnderlyingType(pInfo.PropertyType)); } } else { val = Convert.ChangeType(val, pInfo.PropertyType); } pInfo.SetValue(returnObject, val, null); } } return returnObject; } } public static class DataTableExtensions { public static IList<T> ToList<T>(this DataTable table) where T : new() { IList<PropertyInfo> properties = typeof(T).GetProperties().ToList(); IList<T> result = new List<T>(); foreach (var row in table.Rows) { var item = CreateItemFromRow<T>((DataRow)row, properties); result.Add(item); } return result; } public static IList<T> ToList<T>(this DataTable table, Dictionary<string, string> mappings) where T : new() { IList<PropertyInfo> properties = typeof(T).GetProperties().ToList(); IList<T> result = new List<T>(); foreach (var row in table.Rows) { var item = CreateItemFromRow<T>((DataRow)row, properties, mappings); result.Add(item); } return result; } private static T CreateItemFromRow<T>(DataRow row, IList<PropertyInfo> properties) where T : new() { T item = new T(); foreach (var property in properties) { property.SetValue(item, row[property.Name], null); } return item; } private static T CreateItemFromRow<T>(DataRow row, IList<PropertyInfo> properties, Dictionary<string, string> mappings) where T : new() { T item = new T(); foreach (var property in properties) { if (mappings.ContainsKey(property.Name) && String.IsNullOrEmpty(mappings[property.Name]) != true) property.SetValue(item, row[mappings[property.Name]], null); } return item; } } public static class ICollectionExtensions { public static void AddIfNotExist<T>(this ICollection<T> collection, T item) { if (collection.Contains(item) != true) { collection.Add(item); } } public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> items) { foreach (T item in items) { collection.Add(item); } } public static void AddRangeIfNotExist<T>(this ICollection<T> collection, IEnumerable<T> items) { foreach (T item in items) { collection.AddIfNotExist(item); } } public static bool RemoveRange<T>(this ICollection<T> collection, IEnumerable<T> items) { bool result = true; foreach (T item in items) { result &= collection.Remove(item); } return result; } } }
이직한 회사에서 .NET Framework 2.0을 쓴다. 확장메서드를 못쓰니 정말 불편하다…ㅠ