Press "Enter" to skip to content

Modyfikatory dostępu nie takie święte

Choroba ma swoje dobre strony. Jak akurat nie boli, można się za coś wziąć. Na przykład za napisanie posta na blogu, na którym nie pisało się nic od prawie roku.
No to i piszę;)

Dziś wezmę na tapetę modyfikatory dostępu. Temat zdaje się prosty i oklepany. Mamy ich kilka i są dobrze zdefiniowane:
• Private – dostęp tylko z klasy
• Protected – dostęp z klasy i jej dzieci (klas dziedziczących po klasie)
• Public – dostęp skąd popadnie
• Internal – dostęp w zakresie jednego assembly
• Protected internal– dostęp w zakresie jednego assembly, nielimitowany dla dzieci (dzieci nie muszą być z tego samego assembly, ale nie-dzieci już muszą)

i …

o albo opowiem wam jeszcze jedną anegdotkę.
Dawno, dawno ktoś, nie pamiętam kto, prawdopodobnie więcej niż jedna osoba, w więcej niż jednym momencie powiedziała mi mniej więcej tak: „Wiesz co? Obiektowo to można pisać we wszystkim, nawet w asmie – byle się tylko trzymać konwencji. Procesor i tak nie myśli obiektowo”.

Te wszystkie modyfikatory są więc dla nas- dla programistów, aby było nam łatwiej nadmienionej powyżej konwencji się trzymać. Jakoś łatwiej, kiedy nam kompilator zapewnia hermetyczność sam od siebie, przy naszym niewielkim nakładzie energii, a potem bije nas po łapkach błędami kompilacji, kiedy spróbujemy użyć naszej zmiennej czy funkcji tam, gdzie wg nas nie powinniśmy.

Czy jednak można zadziałać w odwrotny sposób? Załóżmy że chcemy jednak dobrać się do zakazanego owocu bez rażącego po oczkach „wtf is inaccessible due to its protection level”. Czy to możliwe? Oczywiście że tak;) Czy powinniśmy używać tego w kodzie produkcyjnym? Oczywiście że nie;). Czy więc w ogóle podobny myk może przydać nam się w pracy? Pozostawmy to pytanie na trochę później.

Byłam sobie na rozmowie kwalifikacyjnej. Może przemilczę do jakiej firmy, co by im asa z rękawa nie wytrącać. I zostałam zapytana o to, w jaki sposób przetestowałabym metodę prywatną.

No cóż, po pierwsze, to nie widzę potrzeby testowania tego typu metod – skoro według architektury jest ona używana tylko wewnątrz mojej klasy, a nie nigdzie na zewnątrz a klasa powstaje raczej po to, aby ją gdzieś użyć, raczej bym uznała, że jej kod jest przetestowany poprzez pokrycie testami dostępnych na zewnątrz metod, które ich używają. Ale na upartego można i do tego służy pewien myk związany z refleksją.

  • Stwórzmy sobie projekt class library w visual studio o dowolnej nazwie i stwórzmy tam
    metodę o ograniczonym dostępie
  •  przeciążony ToString (aby pokazać, że obiekt się stworzył)
  •  Klasę zagnieżdżoną (nested class) z modyfikatorem private a w niej
    • przeciążonego ToStringa (w tym celu co powyżej)
    • funkcję prywatną

I stwórzmy drugi projekt, dodając referencję do tego pierwszego. A w nim zwykły mały program konsolowy. Tam zwykłym printowaniem udowodnimy, że się da.
I teraz tak. Pozwolę sobie wkleić ów kod i wytłumaczyć na czym ów myk polega:

using System;
using System.Reflection;

namespace olejTo
{
    class Program
    {
        static void Main(string[] args)
        {
            var Asm = Assembly.Load("FindMeIfYouCan");
            var types = Asm.GetTypes();
            var SingleType = types[0];
            var FunnyObject = Convert.ChangeType(Activator.CreateInstance(SingleType), SingleType);
            Console.WriteLine(FunnyObject);
            var MethodInfoF = SingleType.GetMethod("GetHiddenString", BindingFlags.NonPublic | BindingFlags.Instance);
            Console.WriteLine(MethodInfoF.Invoke(FunnyObject, null));
            //to teraz zejdziemy sobie odpowiednio głębiej
            Type nested = SingleType.GetNestedType("HyperSecureClass", BindingFlags.NonPublic | BindingFlags.Instance);
            var StrangeObject = Convert.ChangeType(Activator.CreateInstance(nested), nested);
            var MethodInfoS = nested.GetMethod("GetMoreHiddenString", BindingFlags.NonPublic | BindingFlags.Instance);
            Console.WriteLine(StrangeObject);
            Console.WriteLine(MethodInfoS.Invoke(StrangeObject, null));
            Console.ReadKey();
        }
    }
}

I teraz co się dzieje po kolei.
Pierwszym co należy zrobić, to wyszukać typ w assembly. Kiedy już go mamy, to tworzymy sobie obiekt typu, o jakim tylko wiemy że istnieje. Służy do tego funkcja Activator.CreateInstance, której parametrem jest ów typ. Ja chciałam jeszcze dostać coś bardziej konkretniejszego niż zwracany zwykły object, co nie wiadomo czym jest, więc hmm… no jak mam typ w zmiennej, to nie scastuję podając po prostu tej zmiennej, więc użyłam do tego funkcji Convert.ChangeType, przyjmującej obiekt, oraz zmienną typu. Skojarzenie z np. Convert.ToInt32 nie jest przypadkowe;). ChangeType po prostu jest jej bardziej uniwersalną opcją.

Czas się zabrać za funkcję. Intellisence nam nie podpowie jej (brak na liście) bo przecież jest prywatna ale… no przecież mamy refleksję:).
Pierwsze więc co robimy, to szukamy funkcji o zadanej nazwie (dla odmiany, bo ile można szukać po tablicach), przy czym można mu podać tzw flagi bindowania. Czyli żeby poszukał tam, gdzie zwykle nie zagląda;). No i znalazł, a jak znalazł to zwrócił MethodInfo dla tej funkcji. A jak już mamy MethodInfo, to można je invokenąć… wywołać znaczy się.
Funkcja Invoke z klasy MethodInfo przyjmuje na przykład dwa parametry i są to kolejno: obiekt na rzecz którego mamy wywołać daną funkcję, oraz listę parametrów tej funkcji.

Przejdźmy do trudniejszego przypadku – naszej głęboko ukrytej prywatnej klasy zagnieżdżonej. Przypadek niby bardziej zawiły, jednak analogiczny do poprzedniego. Tyle że pobranie typu wymaga od nas wywołanie na znanym nam już typie funkcji GetNestedType(„nazwa”, bindflagi) i tyle. Jak już mamy typ o jaki nam chodzi, postępujemy analogicznie do poprzedniego. I jaki będzie output wyżej wymienionego programu? To zależy co daliście w funkcjach swojej klasy:)
U mnie wyglądało to tak:

Podsumowując:
Ograniczenia są dla programisty, nie dla maszyny. Maszyna wie, co zrobić z tym co napisaliśmy, to my nie zawsze wiemy co chcemy napisać. Albo nie do końca mamy świadomość tego, co piszemy. A już zwłaszcza jak piszemy program w zespole.
Omijanie podstawowych mechanizmów języka nie jest jakoś specjalnie trudne i czasami się przydaje – choćby w testach.
Oczywiście, zmiana private na public byłaby szybsza;) więc po co sobie utrudniać życie?
Jeszcze jedno – pamiętajcie, że użycie modyfikatorów dostępu nie blokuje możliwości wywołania chronionego kodu- ma to jedynie ułatwić pracę programistom.
Z dwojga złego lepiej kod zmodyfikować niż stosować „hacki”, po to, aby kod był czytelniejszy i łatwiejszy w utrzymaniu.

Link do projektu na githubie: https://github.com/Piatkosia/olejTo

One Comment

  1. Modyfikatory dostępu nie takie święte

    Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl

Comments are closed.