C# jest językiem obiektowym. C# nie pozwala na użycie “funkcji” poza klasami. Jest to wielka różnica w porównaniu z C++ - pierwszym językiem programowania zorientowanym obiektowo. C++ pozwala na pojawianie się funkcji poza klasami ze względu na wsteczną kompatybilność z językiem C.
Można się kłócić, że skoro C# ma klasy statyczne z statycznymi metodami to programowanie imperatywne istnieje, ale jest ono ukryte za obiektową terminologią.
Funkcje w C# mogą istnieć tylko wewnątrz klas i co ważniejsze nie nazywamy je funkcjami, tylko metodami. Metody w C# zawsze muszą coś zwracać. Na szczęście ta zasada jest omijana poprzez typ void. Dzięki temu w metodzie nie musimy używać słowa kluczowego return.
public class Class
{
public void Method()
{
}
}
Teraz czas na ciekawszy fakt. W językach funkcjonalnych, zwłaszcza w tych czystych bez mutacji nie ma klas.
Istnieją oczywiście techniki, które pozwalają na przetrzymywanie danych, ale nie nazywają się one klasami i działają inaczej.
W językach obiektowych wszystko żyje w obiektach, czyli instancjach klasy. W języku funkcjonalnym wszystko żyje w funkcjach.
Może istnieć informacją, która jest lokalna dla danej funkcji jak zmienne zadeklarowane wewnątrz metody w C#, ale nie ma tutaj idei przetrzymywania danych zwłaszcza takich, które ulegają zmianie i istnieją poza obszarem funkcji.
W C# metoda ma tylko dostęp do informacji poza swoim wnętrzem, np. do właściwości lub zmiennych prywatnych, które znajdują się w klasie. Nie jest to zgodne z koncepcją funkcji jaką języki funkcjonalne mają.
Mimo wszystko programowanie funkcjonalne jest możliwe w platformie .NET.
Ponowne użycie funkcji
Ponowne użycie jest jednym z poważniejszych problemów w programowaniu. Jest to jeden z powodów, dla którego języki funkcjonalne powstały, żeby używać ponownie tych samych funkcji i traktować je jak oddzielne kółka zębate.
Oczywiście ponowne użycie nie ogranicza się do programowania funkcjonalnego. W obiektowo zorientowanym programowaniu powtarzające się bloki to klasy. Przy użyciu odpowiednich wzorców możemy używać wciąż tych samych obiektów.
W C# wspiera przeciążanie metod, co w pewnym sensie można potraktować jak modularność funkcji. Od C# 4.0 mamy w metodach opcjonalne parametry.
int Multiplication(int x, int y)
{
return x * y;
}
int Multiplication(int x, int y, int z)
{
return Multiplication(x, y) * z;
}
double Multiplication(double x, double y)
{
return x * y;
}
double Multiplication(double x, double y, double z)
{
return Multiplication(x, y) * z;
}
W tym przykładzie wyraźnie widać, dlaczego przeciążanie metod możemy zestawić z ponownym użyciem już raz napisanej funkcji.
Ten mechanizm pozwala na napisanie nowej funkcji podobnej do tej już istniejącej.
Funkcje te mają tylko inną listę parametrów. Na tej podstawie kompilator wie, której funkcji używamy. W przeciwnym wypadku, gdyby były konflikty kod by się skompilował. Wszystkie przeciążenia nazywają się tak samo, w ten sposób widzimy związek pomiędzy grupą funkcji.
W kontekście algorytmu ponowne użycie może być bardziej skomplikowane. Pomysł jest następujący powinniśmy napisać funkcję, która może być użyta później w taki sposób, w jaki nie była ona początkowo wspierana.
Generalnym pomysłem jest stworzenie funkcji, która będzie zawierała standardowy algorytm, który może być ponownie użyty.
Oto przykład z porównaniem w typowym obiektowym świecie. Mam klasę Person ma ona następujące parametry.
Mam też kobietę i mężczyznę.
public class Person
{
public double Intoversion { get; set; }
public double Extravesion { get; set; }
public double Felling { get; set; }
public double Thinking { get; set; }
public double Judging { get; set; }
public double Perception { get; set; }
}
public class Man : Person
{}
public class Woman : Person
{}
Oto klasa reprezentująca wynik porównania.
public class MatchResult { public Man Man { get; set; } public Woman Woman { get; set; } }
A oto mój algorytm porównania obydwu osób, czy nadają się na związek.
public static class MatchMaker
{
public static List<MatchResult> Match(
List<Woman> girls, List<Man> boys)
{
List<MatchResult> list = new List<MatchResult>();
foreach (var boy in boys)
{
foreach (var girl in girls)
{
double chance = 0;
if (boy.Felling >= girl.Felling)
chance += 0.2;
if (girl.Judging <= boy.Thinking)
chance += 0.2;
if (girl.Intoversion >= boy.Intoversion)
chance += 0.2;
if (boy.Perception <= girl.Judging)
chance += 0.2;
if (boy.Extravesion >= girl.Extravesion)
chance += 0.2;
if (chance > 0.6)
{
list.Add(new MatchResult()
{
Man = boy,
Woman = girl,
});
}
}
}
return list;
}
}
Jeśli się przyjrzysz temu algorytmowi, możesz zobaczyć, że pewna jego cześć to mechanizm porównania. Mechanizm zestawienia dwóch obiektów i zapisania ich mógłby być później użyty w innym kontekście, niż serwisu randkowego.
Czy nie byłoby wspaniale, gdyby ta metoda porównująca była bardziej elastyczna i przyjmowała każdy typ danych?
W zależności od języka istnieje wiele sposobów na zastąpienie specyficznego typu na jego wersję mniej specyficzną. W obiektowym świecie jest to klasa bazowa. W starszym języku typu C można byłoby się posłużyć wskaźnikiem.
Problem jest jednak ten sam, tracimy informację.
Obecnie mój algorytm wie jak działać z kobietami i facetami, ale klasa bazowa object już o tych właściwościach nic nie wie.
Ten problem omówię później we wpisie z typami generycznymi. Na razie go nie rozwiążemy.
Oto nasza nowa klasa mówiąca o rezultacie porównania.
public class MatchResult
{
public object ItemOne { get; set; }
public object ItemTwo { get; set; }
}
Pomysł jest więc taki, nasz algorytm powinien się nie przejmować jakie obiekty są w liście. Inna cześć aplikacji będzie zawierać tę informację.
public static class MatchMaker
{
public static List<MatchResult> Match(
List<object> elementsOne, List<object> elementsTwo)
{
List<MatchResult> list = new List<MatchResult>();
foreach (var boy in elementsTwo)
{
foreach (var girl in elementsOne)
{
//??
}
}
return list;
}
}
Oto inne klasy.
public class Customer : Person
{
public double Needs{ get; set; }
}
public class Computer
{
public double Power { get; set; }
}
Oto metody porównujące dane.
public static bool MatchLogic_Customer(object a, object b)
{
return ((Customer)a).Needs == ((Customer)b).Needs;
}
public static bool MatchLogic_Computer(object a, object b)
{
return ((Computer)a).Power == ((Computer)b).Power;
}
public static bool MatchLogic_String(object a, object b)
{
return ((string)a).Length - 1 == ((string)b).Length ||
((string)b).Length -1 == ((string)a).Length ||
((string)a).Length == ((string)b).Length));
}
Każda z tych funkcji zakłada, że otrzyma specyficzny typ danych. Co nie jest dobrym pomysłem. Zobaczymy później jak to można rozwiązać.
Możemy obecnie użyć jednej z tych funkcji.
public static class MatchMaker
{
public static List<MatchResult> Match(
List<object> elementsOne, List<object> elementsTwo)
{
List<MatchResult> list = new List<MatchResult>();
foreach (var element1 in elementsTwo)
{
foreach (var element2 in elementsOne)
{
if (MatchLogic_String(element1, element2))
list.Add(new MatchResult()
{
ItemOne = element1,
ItemTwo = element2
});
}
}
return list;
}
Problem jednak pozostaje. Wszystko zależy od tego, jaki typ będziemy przechowywać w liście.
Częściowo możemy ten problem rozwiązać używając delegaty.
public static class MatchMaker
{
public delegate bool IsMatch(object a, object b);
public static List<MatchResult> Match(
List<object> elementsOne, List<object> elementsTwo,
IsMatch isMatch)
{
List<MatchResult> list = new List<MatchResult>();
foreach (var element1 in elementsTwo)
{
foreach (var element2 in elementsOne)
{
if (isMatch(element1, element2))
list.Add(new MatchResult()
{
ItemOne = element1,
ItemTwo = element2
});
}
}
return list;
}
Wyrażenie public delegate nie jest częścią samej funkcji; może żyć wszędzie. Ważne jest jednak to, że moja funkcja Match przyjmuje funkcję jako parametr.
Jest to możliwe dzięki delegatom, można się do nich referować jako typ funkcyjny .
Delegata definiuje, że w tym miejscu ma się pojawić funkcja, która przyjmuje dwa obiekty i zwraca wartość logiczną.
Jeśli na pewnym etapie aplikacji typy, które będą w liście są znane, to ta funkcja wykona właściwe porównanie.
Funkcja typu „Match”, która została tutaj napisana nazywa się funkcją wyższego rzędu.
Funkcja wyższego rzędu może zwracać kolejną nową funkcję zamiast parametrów.
O czym później.