LazyCzęść NR.10Czy jest możliwe w C# uruchomienie metody, tylko wtedy, gdy jest ona nam potrzebna.

Wartościowanie leniwe jest skomplikowanym zagadnieniem i ciężko jest go zrozumieć bez odpowiedniego przykładu w kodzie.

 

 

Powiedzmy, że ma następującą funkcję:

Math.Sqrt(5 * 4 * 5);

Parametr przekazany jest tak naprawdę operacją mnożenia. Operacja ta musi się wykonać przed wykonywaniem metody Sqrt. Tylko w taki sposób można metodą uzyskać ten parametr.

Co w tym złego? Nic, ale co, jeśli ta funkcja Sqrt ostatecznie nie potrzebowała tego parametru? Jeśli tak by było, to operacja mnożenia przed wykonaniem metody była całkowicie zbędna.

Koncepcja ta nie jest niczym nowym. Przykładowo C# implementuje tę filozofię w trakcie sprawdzenia warunków LUB.

bool isTrue = (100 > 1) || (Math.Sqrt(5 * 4 * 5) == 10) 
|| OtherBigMethod();

Jeśli pierwszy warunek jest prawdziwy to C# nie wykonuje pozostałych metod wyrażeniu LUB.

Ma to sens, gdyż pozostałe wywołania nie są potrzebne ponieważ już na tym etapie wiemy, że wyrażenie zawsze będzie prawdziwe.

Czy C# oferuje coś jeszcze? Niestety nie. Nie oznacza to jednak, że technika wartościowania leniwego została całkowicie pominięta. Od C# 3.0 mamy kolekcje implementujące IEnumerator oraz całą architekturę LINQ, która pozwala na opóźniony dostęp do kolekcji do momentu, gdy jest ona potrzebna.

Jaki to ma sens?

Operujemy wtedy na obietnicach, które zostaną wykonane dopiero, gdy są potrzebne. Jeśli nie były one potrzebne, wtedy unikamy wykonanie zbędnego kodu.  To samo można zrobić z metodami, tylko jak?

Wyślij funkcje

Najprostszym sposobem na wartościowanie leniwe jest przesłanie funkcji jako parametru.  Spójrz na poniższy kod:

private static int LongCalculation()
{
    Console.WriteLine("LongCalculation");
    return 2;
}

private static int AnotherLongCalculation()
{
    Console.WriteLine("AnotherLongCalculation");
    return 3;
}
private static void HighterMethod(int a, int b, int c)
{
    Console.WriteLine("HighterMethod");
    if (a != 0)
        Console.WriteLine(b * c);
}

Mamy dwie funkcje, które hipotecznie bardzo długo liczą pewną liczbę. Mamy też trzecią funkcję, która wykona pewną operację pod warunkiem, że parametr “a” nie jest równy zero.

Teraz wywołujemy funkcję w następujący sposób.

HighterMethod(0, LongCalculation(), 
    AnotherLongCalculation());

Tutaj zauważamy problem. Wiemy, że długie kalkulacje nie będą nam potrzebne ponieważ parametry b i c nie będą nigdzie używane wewnątrz tej funkcji ponieważ parametr “a” równa się zero.

image

Jak ten problem rozwiązać. Wewnątrz metody HighterMethod obecnie żaden kod nie sprawi, aby te metody wcześniej się nie uruchomiły.

Chyba, że byśmy przesyłali niegotowe parametry a funkcje, które mają te parametry zwrócić.

private static void HighterMethod(Func<int> a, 
    Func<int> b,
    Func<int> c)
{
    Console.WriteLine("HighterMethod");
    if (a() != 0)
        Console.WriteLine(b() * c());
}

Teraz funkcje będą wykonywane tylko wtedy, gdy parametry są potrzebne.  Wywołanie metody wygląda teraz tak.

HighterMethod(() => { return 0; }, LongCalculation, 
    AnotherLongCalculation);

Jak widać nie kłamię bo rzeczywiście teraz to tak działa. Konsola nie wydrukowała innych napisów ponieważ inne metody się nie wykonały

image

Lazy<T>

To była jedna z technik na rozwiązanie tego problemu. W .NET 4.0 mamy do dyspozycji coś naprawdę potężnego, a jest nim klasa generyczna Lazy.

W projekcie testującym kod mam także bibliotekę FCSlib, która także zawiera taką klasę generyczną.

image

Z tego co wiem obie te klasy są inne, ale ich cel jest taki sam. Implementacja z biblioteki FCSlib została napisana z myślą o mechanizmach z F#. Widać, że posiada ona więcej opcji niż implementacja z bibliotek .NET.

Lazy z FCSlib jak widać posiada wbudowaną informacje o wyjątkach, jeśli one wystąpią. Jeśli nasza metoda zwróciła błąd, to każde odwołanie do wartości wywoła ten wyjątek.

image

Lazy z .NET w C#.

image

Obie implementacje Lazy<T> są bezpieczne wątkowo.  Użycie Lazy<T> jest bardzo proste. Wewnątrz parametru możemy umieścić funkcje, które zostaną wykonane dopiero, gdy wartość i wynik metody jest potrzebny.

var lazy = new System.
    Lazy<string>(() => LongRunningMethod());

Tak rozwiązujemy problem z wartościowaniem leniwym.

var r01 = new System.Lazy<int>(LongCalculation);
var r02 = new System.Lazy<int>(AnotherLongCalculation);

Istnieje też pewna inna zaleta wynikająca z korzystania z Lazy<T>

var r01 = new System.Lazy<int>(LongCalculation);
var r02 = new System.Lazy<int>(AnotherLongCalculation);

var r03 = new FCSlib.Lazy<int>(LongCalculation);
var r04 = new FCSlib.Lazy<int>(AnotherLongCalculation);

int[] tab = new[] { r01.Value,
r02.Value,
r03.Value,
r04.Value };

int[] tab2 = new[] { r01.Value,
r02.Value,
r03.Value,
r04.Value };

Nieważne ile razy będziemy się odwoływać do wartości –> metoda tworząca tę wartość zostanie wykonana tylko raz. Wyniki metod i funkcji zostają zapisane i zwracane ponownie.

Jak widać metody uruchomiły się tylko 2 razy: raz dla Lazy<T> z .NET i raz dla biblioteki FCSLib.

image

Lazy<T> może zostać użyte jako alternatywna implantacja wzorca MonoState. Kod potrzebny do deklaracji pola prywatnego zostanie uruchomiony dopiero, gdy właściwość będzie potrzebna.

public class SuperObject
{
}

public class Super
{
    private System.Lazy<SuperObject> _ourSuperObject = 
        new System.Lazy<SuperObject>(() =>
    {
        var awesomeObject = new SuperObject();
        //do stuff here to build your awesomeObject
        return awesomeObject;
    });

    public SuperObject OurAwesomeObject
    {
        get { return _ourSuperObject.Value; }
    }
}

Dla porównania oto stary sposób implementacji wzorca Monostate.

public class Super2
{
    private SuperObject _ourSuperObject = null;

    public SuperObject OurAwesomeObject
    {
        get
        {
            if (_ourSuperObject == null)
                _ourSuperObject = new SuperObject();

            return _ourSuperObject;
        }
    }
}

Zalecam napisanie funkcji pomocniczych gdybyś chciał na poważnie owijać swoje metody w generyczny typ Lazy<T>

private static System.Lazy<int> LazyAdd(Func<int> fun)
{
    Console.WriteLine("Calling LazyAdd");
    return new System.Lazy<int>(fun);
}

Pamiętaj jednak, aby nie zawsze korzystać z Lazy<T>. Prawda, że twoja aplikacja będzie działać szybciej, jeśli opóźnisz wykonywanie pewnych funkcji. Z drugiej strony gdybyś opóźnił wykonywanie wszystkich funkcji, to byś miał przegrzanie wynikające z działania zamków na wątkach stosowanych w Lazy<T>.

Nie zapominaj, że Lazy<T> tylko opóźnia wykonywanie kodu i wysyła jego egzekucję w innym czasie. Kod ten jednak będzie musiał się kiedyś uruchomić, jeśli zajdzie taka potrzeba.

To wszystko na temat wartościowania leniwego.