C# 7.0 – lokalne funkcje

Dzisiaj zaczynam pierwszy wpis o nowościach w C# 7.0. Przede wszystkim warto ściągnąć Visual Studio 15 Preview. Wersja 15, to przyszły następca Visual Studio 2015, który określany był wersją 14.

Jeśli chcemy sprawdzić nowości z C# 7.0 musimy najpierw ustawić  __DEMO__ oraz  __DEMO_EXPERIMENTAL__  we właściwościach projektu:

1

Pierwsza nowość to zagnieżdżone funkcję lokalne. Przykład:

    public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a)
            {

                return a;
            }

            Console.WriteLine(GetValue(5));
        }
    }

Innymi słowy jest to funkcja w funkcji. Czasami deklarujemy prywatną metodę, która używana jest jedynie w jednej metodzie jako helper. Za pomocą lokalnej funkcji w łatwy sposób możemy ograniczyć jej zasięg.

Funkcja jest lokalna, zatem poniższy kod nie skompiluje się:

    public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a)
            {

                return a;
            }

            Console.WriteLine(GetValue(5));
        }

        public void DoSomething1()
        {
            Console.WriteLine(GetValue(5));
        }
    }

Lokalną funkcję można tylko wywoływać w metodzie, w której ją zadeklarowano. Analogicznie poniższy kod wywoła błąd kompilacji:

    class Program
    {
        private static void Main(string[] args)
        {
            Test test = new Test();
            test.DoSomething();
            test.GetValue(5);
        }
    }

Co prawda można wywołać test.DoSomething, ale test.GetValue już nie.
Możliwe jest również deklarowanie funkcji zagnieżdżonych wielokrotnie, tzn.:

   public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a)
            {
                string GetString(string b)
                {
                    return b;
                }

                Console.WriteLine(GetString(a.ToString()+" as text"));

                return a;
            }

            Console.WriteLine(GetValue(5));
        }
    }

Na ekranie najpierw wyświetli się “5 as text”, a potem 5. Z kolei następujący kod już nie skompiluje się:

    public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a)
            {
                string GetString(string b)
                {
                    return b;
                }                

                return a;
            }

            Console.WriteLine(GetString(a.ToString() + " as text"));
        }
    }

Oznacza to, że z funkcji A można jedynie odnosić się do tej najbardziej zewnętrznej funkcji lokalnej. Nic z kolei nie stoi na przeszkodzie, aby zadeklarować kilka metod o tym samym stopniu zagnieżdżenia:

    public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a)
            {              
                return a;
            }

            string GetString(string b)
            {
                return b;
            }


            Console.WriteLine(GetString("5 as text"));
            Console.WriteLine(GetValue(5));
        }
    }

Jakiekolwiek modyfikatory są zabronione, np. taki kod nie skompiluje się:


    public class Test
    {
        public void DoSomething()
        {
            static int GetValue(int a)
            {              
                return a;
            }
            Console.WriteLine(GetValue(5));
        }
    }

Analogicznie sprawa wygląda z public czy nawet private.
Możliwe jest z kolei z korzystanie z metod wyrażonych w formie lambda (nowość z c# 6.0):

    public class Test
    {
        public void DoSomething()
        {
            int GetValue(int a) => a;
      
            Console.WriteLine(GetValue(5));
        }
    }

Funkcja lokalna może mieć również dostęp do zmiennych zadeklarowanych w zewnętrznej funkcji, tzn.:

       public class Test
    {
        public void DoSomething()
        {
            int outerValue = 43;

            void Nested()
            {
                Console.WriteLine(outerValue);
                outerValue = 100;
            }

            Nested();
            Console.WriteLine(outerValue);
        }
    }

Na ekranie wyświetli się 43, a potem 100, ponieważ wartości kopiowane są w analogiczny sposób jak to ma miejsce w anonimowych funkcjach. Kolejne zagnieżdżenia mogą korzystać ze zmiennych zadeklarowanych w nadrzędnych funkcjach.

Na zakończenie warto spojrzeć co naprawdę jest wygenerowane w tle:

.class /*02000003*/ public auto ansi beforefieldinit 
  ConsoleApplication1.Test
    extends [mscorlib]System.Object
{

  .method /*06000003*/ public hidebysig instance void 
    DoSomething() cil managed 
  {
    .maxstack 8

    // [22 9 - 22 10]
    IL_0000: nop          
    IL_0001: nop          

    // [27 13 - 27 44]
    IL_0002: ldc.i4.5     
    IL_0003: call         int32 ConsoleApplication1.Test::'<DoSomething>g__GetValue0_0'(int32)
    IL_0008: call         void [mscorlib]System.Console::WriteLine(int32)
    IL_000d: nop          

    // [28 9 - 28 10]
    IL_000e: ret          

  } // end of method Test::DoSomething

  .method /*06000004*/ public hidebysig specialname rtspecialname instance void 
    .ctor() cil managed 
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [mscorlib]System.Object::.ctor()
    IL_0006: nop          
    IL_0007: ret          

  } // end of method Test::.ctor

  .method /*06000005*/ assembly hidebysig static int32 
    '<DoSomething>g__GetValue0_0'(
      /*08000002*/ int32 a
    ) cil managed 
  {
    .custom /*0C00000F*/ instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() /*06003F5F*/ 
      = (01 00 00 00 )
    .maxstack 1
    .locals init (
      [0] int32 V_0
    )

    // [24 13 - 24 14]
    IL_0000: nop          

    // [25 17 - 25 26]
    IL_0001: ldarg.0      // a
    IL_0002: stloc.0      // V_0
    IL_0003: br.s         IL_0005

    // [26 13 - 26 14]
    IL_0005: ldloc.0      // V_0
    IL_0006: ret          

  } // end of method Test::'<DoSomething>g__GetValue0_0'
} // end of class ConsoleApplication1.Tes

Warto zwrócić uwagę na dwie sygnatury:

 .method /*06000003*/ public hidebysig instance void 
    DoSomething() cil managed 
[/csharp

oraz


 .method /*06000005*/ assembly hidebysig static int32 
    '<DoSomething>g__GetValue0_0'(
      /*08000002*/ int32 a
    ) cil managed 

.

Innymi słowy, w praktyce dwie różne metody są generowane (lokalna funkcja to ta prywatna).

4 thoughts on “C# 7.0 – lokalne funkcje”

  1. Szczerze mówiąc mam dość mieszane odczucia, jeśli chodzi o takie rozwiązanie. Wydaje mi się, że metody korzystające z takiego rozwiązania będą zbyt rozwlekłe i mało czytelne.

  2. Nie bardzo widzę zastosowanie dla takich funkcji…
    W praktyce to to samo co funkcja prywatna, którą też można zapisać lambda więc zmienia to naprawdę niewiele. Istotną różnicą jest dostęp do zmiennych lokalnych zewnętrznej funkcji, ale i to można rozwiązać zwykłym parametrem…

Leave a Reply

Your email address will not be published.