본문 바로가기

진리는어디에/C#

[C#] Local Function

이번 포스트는 C#의 로컬 함수(local function)이라는 개념에 대해서 살펴 보도록한다. 초보자 대상이 아닌 어느 정도 C#과 프로그래밍에 대한 개념이 있는 사람을 대상으로 작성하였으므로 개념이나 코드에 관련된 설명 중 기본적인 부분은 생략되어 있는 부분이 많다. 이해가 잘 안가는 부분들에 대해서 댓글로 남겨 주시면 성실히 답변해 드리도록 하겠다.

Local Function이란?

C# 7.0 부터 추가 된 개념으로써 메소드 안에 메소드를 정의 할 수 있는 문법이다.

  • 메소드 안에 다시 메소드를 만드는 문법
  • 자신이 포함된 메소드에서만 호출할 수 있다.
public double Divide(double a, double b)
{
    double real_devide(double a, double b)  // local function
    {
        return a / b;
    }
    
    if (0 == b)
    {
        throw new System.Exception("zero devided exception");
    }
    return real_devide(a, b);
}

그럼 메소드 안에 메소드를 또 만들어서 어디 쓰는 것인가? 공식 가이드에서는 위 예처럼 메소드 내에서 로직을 명확하게 구분하기 위해 사용한다는데 사실 이건 얼마나 효용성이 있는지 잘 모르겠다.

오히려 이터레이터를 만들거나 비동기 메소드 처럼 메소드의 호출 시점과 실행 시점이 다를 경우, 호출 시점에 뭔가(정합성 체크, 초기화 등등)를 해주기 위해 사용하는 것이 더 유용 할 것 같다.

로컬 함수는 이터레이터나 비동기 메소드에서 사용하면 좋다.

활용

먼저 아래와 같은 콜렉션 클래스가 있다고 가정하자. 별거 아니다. 그냥 1부터 5까지 순서대로 돌려주는 코루틴으로 만들어진 이터레이터(iterator)다.

using System;
using System.Collections;

class NumCollections : IEnumerable
{
    private int[] arr = { 1, 2, 3, 4, 5 };

    public IEnumerator GetEnumerator()
    {
        Console.WriteLine("Call GetEnumerator()");
        foreach (int n in arr)
        {
            yield return n;
        }
    }
}

class Program
{
    public static void Main()
    {
        NumCollections nums = new NumCollections();
        IEnumerator itr = nums.GetEnumerator(); // 이터레이터 객체 생성'만' 함
        Console.WriteLine("After Getting Enumerator");
        while (itr.MoveNext())  // 여기서 이터레이터 실행
        {
            Console.WriteLine(itr.Current);
        }
    }
}

// OUTPUT :
// Call GetEnumerator()
// After Getting Enumerator
// 1
// 2
// 3
// 4
// 5

코루틴의 특징은 '게으른 평가'로써, GetEnumerator() 메소드로 이터레이터 객체를 얻어 오는 시점에서는 코루틴이 전혀 실행 되지 않고 MoveNext()를 호출하는 시점에서야 호출 된다는 것이다.

이제 위 NumCollections 클래스에 출력할 arr가 정상적인 객체를 참조하고 있는지 체크하느 로직이 필요하다고 가정하자.

    public IEnumerator GetEnumerator()
    {
        Console.WriteLine("Call GetEnumerator()");
        if (arr == null)
        {
            throw new Exception("null");
        }

        foreach (int n in arr)
        {
            yield return n;
        }
    }

하지만 위와 같은 코드는 이어터레이터 객체를 생성할 때 체크 되는 것이 아니라 MoveNext()를 실행 할 때, 결국, 최초 실행 시점이 되어서야 오류를 체크 할 수 있다는 것이다. 이때 로컬 함수를 사용하면 코드를 깔끔하게 만들 수 있다.

    public IEnumerator GetEnumerator()
    {
        Console.WriteLine("Call GetEnumerator()");
        if (arr == null)
        {
            throw new Exception("null");
        }

        return Impl();
        IEnumerator Impl()
        {
            foreach (int n in arr)
            {
                yield return n;
            }
        }
    }
    
// OUTPUT :
// Call GetEnumerator()
// After Getting Enumerator
// 1
// 2
// 3
// 4
// 5

위 예제의 GetEnumerator()는 yield를 사용하지 않으므로 코루틴이 아닌 일반 메소드로 취급 되어 호출 시점에 바로 실행 되고, 실제 코루틴인 Impl 클래스의 객체를 리턴한다. 그래서 일단 예외 체크 코드는 다 실행 하고 Impl에서 일단 중단 된다.

결과로 GetEnumerator() 내부의 로그가 먼저 찍히는 것을 확인 할 수 있다. 비동기 메소드 역시 연장선상에서 생각하면 된다.

정적 로컬 펑션

일반 로컬 함수를 사용하게 되면 인자 뿐만 아니라 자신이 속한 메소드의 로컬 변수에도 접근이 가능하다.

public  int Foo(int a, int b)
{
    int value = 10;
    
    return Bar(10);
    int Bar(int c)
    {
        return value + a + b + c; // Foo의 value, a, b 모두 접근 가능
    }
}

8.0에서는 정적 로컬 함수(static local function)이라는 것이 추가 었다. 로컬 함수의 정의에 static 키워드를 붙여 주면 자신 로컬 변수만 접근 가능한다. 외부 함수의 로컬 변수에 접근 하려 하면 오류가 발생한다.

public  int Foo(int a, int b)
{
    int value = 10;
    
    return Bar(10);
    static int Bar(int c)
    {
        return value + a + b + c;
    }
}

// Build Error
// 정적 로컬 함수는 'value'에 대한 참조를 포함할 수 없습니다.
// 정적 로컬 함수는 'b'에 대한 참조를 포함할 수 없습니다.
// 정적 로컬 함수는 'a'에 대한 참조를 포함할 수 없습니다.
  • Non-static Local function : 자신을 포함하고 있는 메소드의 지역 변수에 접근할 수 있다.
  • Static Local function : 자신을 포함하고 있는 메소드의 지역변수에 접근할 수 없다. C# 8.0 에 추가된 문법

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!