Co możesz zrobić w MSIL, czego nie możesz zrobić w C # lub VB.NET? [Zamknięte]

165

Cały kod napisany w językach .NET kompiluje się do MSIL, ale czy istnieją określone zadania / operacje, które można wykonać tylko bezpośrednio przy użyciu MSIL?

Zróbmy też rzeczy łatwiejsze w MSIL niż C #, VB.NET, F #, j # czy jakikolwiek inny język .NET.

Jak dotąd mamy to:

  1. Rekursja ogona
  2. Ogólna współ / kontrawariancja
  3. Przeciążenia, które różnią się tylko typami zwracanymi
  4. Zastąp modyfikatory dostępu
  5. Mieć klasę, która nie może dziedziczyć po System.Object
  6. Filtrowane wyjątki (można to zrobić w vb.net)
  7. Wywołanie metody wirtualnej bieżącego typu klasy statycznej.
  8. Zapoznaj się z pudełkową wersją typu wartości.
  9. Spróbuj / błąd.
  10. Używanie zabronionych nazw.
  11. Zdefiniuj własne konstruktory bez parametrów dla typów wartości .
  12. Zdefiniuj zdarzenia za pomocą raiseelementu.
  13. Niektóre konwersje dozwolone przez środowisko CLR, ale nie przez C #.
  14. Utwórz main()metodę inną niż .entrypoint.
  15. pracować bezpośrednio z typami natywnymi inti natywnymi unsigned int.
  16. Graj z przejściowymi wskaźnikami
  17. emitbyte w MethodBodyItem
  18. Wyrzuć i złap typy inne niż System.Exception
  19. Dziedzicz wyliczenia (niezweryfikowane)
  20. Tablicę bajtów można traktować jako (4x mniejszą) tablicę liczb całkowitych.
  21. Możesz mieć wszystkie pola / metody / właściwości / zdarzenia o tej samej nazwie (niezweryfikowane).
  22. Możesz rozgałęzić się z powrotem do bloku try z własnego bloku catch.
  23. Masz dostęp do specyfikatora dostępu famandassem ( protected internalto fam lub assem)
  24. Bezpośredni dostęp do <Module>klasy w celu zdefiniowania funkcji globalnych lub inicjatora modułu.
Binoj Antony
źródło
17
Świetne pytanie!
Tamas Czinege
5
F # obsługuje rekurencję ogona, patrz: en.wikibooks.org/wiki/F_Sharp_Programming/Recursion
Bas Bossink
4
Dziedziczyć wyliczenia? To byłoby czasami fajne ...
Jimmy Hoffa
1
Metoda główna ma duże M w .NET
Concrete Gannet
4
Twierdzenie „zamknięte jako niekonstruktywne” jest absurdalne. To jest pytanie empiryczne.
Jim Balter

Odpowiedzi:

34

MSIL zezwala na przeciążenia, które różnią się tylko typami zwracanymi z powodu

call void [mscorlib]System.Console::Write(string)

lub

callvirt int32 ...
Anton Gogolev
źródło
5
Skąd znasz takie rzeczy? :)
Gerrie Schenck
8
To jest niesamowite. Kto nie chciał powodować przeciążeń powrotu?
Jimmy Hoffa
12
Jeśli dwie metody są identyczne z wyjątkiem typu zwracanego, czy można je wywołać z C # lub vb.net?
supercat
29

Większość języków .Net, w tym C # i VB, nie korzysta z funkcji rekurencji ogonowej kodu MSIL.

Rekursja ogona to optymalizacja, która jest powszechna w językach funkcjonalnych. Występuje, gdy metoda A kończy się zwracaniem wartości metody B, tak że stos metody A może zostać cofnięty po wywołaniu metody B.

Kod MSIL wyraźnie obsługuje rekurencję ogonową, a dla niektórych algorytmów może to być ważna optymalizacja do wykonania. Ale ponieważ C # i VB nie generują instrukcji, aby to zrobić, należy to zrobić ręcznie (lub za pomocą F # lub innego języka).

Oto przykład, jak rekurencja ogona może być implementowana ręcznie w C #:

private static int RecursiveMethod(int myParameter)
{
    // Body of recursive method
    if (BaseCase(details))
        return result;
    // ...

    return RecursiveMethod(modifiedParameter);
}

// Is transformed into:

private static int RecursiveMethod(int myParameter)
{
    while (true)
    {
        // Body of recursive method
        if (BaseCase(details))
            return result;
        // ...

        myParameter = modifiedParameter;
    }
}

Powszechną praktyką jest usuwanie rekursji przez przenoszenie danych lokalnych ze stosu sprzętowego do struktury danych stosu z alokacją sterty. W eliminacji rekurencji tail-call, jak pokazano powyżej, stos jest całkowicie eliminowany, co jest całkiem dobrą optymalizacją. Ponadto wartość zwracana nie musi przechodzić przez długi łańcuch wywołań, ale jest zwracana bezpośrednio.

W każdym razie CIL udostępnia tę funkcję jako część języka, ale w przypadku C # lub VB musi być ona zaimplementowana ręcznie. (Jitter może również samodzielnie przeprowadzić tę optymalizację, ale to zupełnie inna kwestia).

Jeffrey L Whitledge
źródło
1
F # nie korzysta z rekurencji końcowej MSIL, ponieważ działa tylko w przypadkach w pełni zaufanych (CAS) ze względu na sposób, w jaki nie opuszcza stosu w celu sprawdzenia przypisań uprawnień (itp.).
Richard
10
Richard, nie jestem pewien, co masz na myśli. F # z pewnością emituje ogon. prefiks wywołania, prawie w każdym miejscu. Sprawdź IL pod kątem tego: "let print x = print_any x".
MichaelGG
1
Uważam, że JIT i tak będzie używał rekurencji ogonowej w niektórych przypadkach - aw niektórych zignoruje wyraźne żądanie tego. To zależy od architektury procesora, IIRC.
Jon Skeet
3
@Abel: Chociaż architektura procesora jest teoretycznie nieistotna, w praktyce nie jest bez znaczenia, ponieważ różne JIT dla różnych architektur mają różne reguły rekurencji ogonowej w .NET. Innymi słowy, możesz bardzo łatwo mieć program, który wybuchnie na x86, ale nie na x64. To, że rekurencję ogonową można zaimplementować w obu przypadkach, nie oznacza, że ​​tak jest . Zwróć uwagę, że to pytanie dotyczy w szczególności platformy .NET.
Jon Skeet
2
C # faktycznie obsługuje wywołania tail pod x64 w określonych przypadkach: community.bartdesmet.net/blogs/bart/archive/2010/07/07/… .
Pieter van Ginkel
21

W MSIL możesz mieć klasę, która nie może dziedziczyć po System.Object.

Przykładowy kod: skompiluj go za pomocą ilasm.exe AKTUALIZACJA: Musisz użyć "/ NOAUTOINHERIT", aby zapobiec automatycznemu dziedziczeniu asemblera.

// Metadata version: v2.0.50215
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 2:0:0:0
}
.assembly sample
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module sample.exe
// MVID: {A224F460-A049-4A03-9E71-80A36DBBBCD3}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x02F20000


// =============== CLASS MEMBERS DECLARATION ===================

.class public auto ansi beforefieldinit Hello
{
  .method public hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       13 (0xd)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      "Hello World!"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ret
  } // end of method Hello::Main
} // end of class Hello
Ramesh
źródło
2
@Jon Skeet - Z całym szacunkiem, czy mógłbyś mi pomóc zrozumieć, co oznacza NOAUTOINHERIT. MSDN określa „Wyłącza domyślne dziedziczenie po Object, jeśli nie określono żadnej klasy bazowej. Nowość w .NET Framework w wersji 2.0”.
Ramesh
2
@Michael - Pytanie dotyczy MSIL, a nie wspólnego języka pośredniego. Zgadzam się, że może to nie być możliwe w CIL, ale nadal działa z MSIL
Ramesh
4
@Ramesh: Ups, masz całkowitą rację. Powiedziałbym, że w tym momencie łamie standardową specyfikację i nie powinien być używany. Reflektor nawet nie ładuje asmeble. Jednak można to zrobić za pomocą ilasm. Zastanawiam się, dlaczego tam jest.
Jon Skeet
4
(Ach, widzę, że fragment / noautoinherit został dodany po moim komentarzu. Przynajmniej czuję się trochę lepiej, nie zdając sobie z tego sprawy wcześniej ...)
Jon Skeet
1
Dodam, że przynajmniej na .NET 4.5.2 w Windows kompiluje się, ale nie wykonuje ( TypeLoadException). PEVerify zwraca: [MD]: Error: TypeDef, który nie jest interfejsem, a nie klasą Object rozszerza token Nil.
xanatos
20

Możliwe jest łączenie modyfikatorów protectedi internaldostępu. W C #, jeśli napiszesz, protected internalelement członkowski jest dostępny z zestawu iz klas pochodnych. Via MSIL można uzyskać członka, który jest dostępny z klas pochodnych w zespole tylko . (Myślę, że to może być całkiem przydatne!)

yatima2975
źródło
4
Jest to kandydat do zaimplementowania w C # 7.1 ( github.com/dotnet/csharplang/issues/37 ), modyfikatorem dostępu jestprivate protected
Happypig375
5
Został wydany jako część C # 7.2: blogs.msdn.microsoft.com/dotnet/2017/11/15/…
Joe Sewell
18

Och, wtedy tego nie zauważyłem. (Jeśli dodasz tag jon-skeet, jest to bardziej prawdopodobne, ale nie sprawdzam go tak często).

Wygląda na to, że masz już całkiem dobre odpowiedzi. Dodatkowo:

  • Nie można uzyskać dojścia do pudełkowej wersji typu wartości w C #. Możesz w C ++ / CLI
  • Nie można wykonać próby / błędu w C # („błąd” to coś w rodzaju „złap wszystko i wyrzuć ponownie na końcu bloku” lub „ostatecznie, ale tylko w przypadku niepowodzenia”)
  • Istnieje wiele nazw, które są zabronione przez C #, ale prawna IL
  • IL umożliwia definiowanie własnych konstruktorów bez parametrów dla typów wartości .
  • Nie można definiować zdarzeń za pomocą elementu „raise” w C #. (W VB ty masz do niestandardowych zdarzeń, ale „default” wydarzenia nie obejmować jeden).
  • Niektóre konwersje są dozwolone przez środowisko CLR, ale nie przez C #. Jeśli przejdziesz przez objectC #, te czasami będą działać. Zobacz przykład pytania uint [] / int [] SO .

Dodam do tego, jeśli pomyślę o czymś innym ...

Jon Skeet
źródło
3
Ach, metka jon-skeet, wiedziałem, że czegoś mi brakuje!
Binoj Antony
Aby użyć nielegalnej nazwy identyfikatora, możesz poprzedzić ją
znakiem
4
@George: To działa w przypadku słów kluczowych, ale nie wszystkich prawidłowych nazw IL. Spróbuj podać <>anazwę w języku C # ...
Jon Skeet
17

Środowisko CLR obsługuje już ogólną współbieżność / kontrawariancję, ale C # nie otrzymuje tej funkcji do 4,0

ermau
źródło
Czy możesz podać link z dodatkowymi informacjami na ten temat?
Binoj Antony,
14

W IL możesz wrzucić i złapać dowolny typ, nie tylko typy pochodzące od System.Exception.

Daniel Earwicker
źródło
6
Możesz to zrobić również w C #, z try/ catchbez nawiasów w instrukcji catch, aby złapać również wyjątki inne niż wyjątki. Jednak rzucanie jest rzeczywiście możliwe tylko wtedy, gdy dziedziczysz z Exception.
Abel
@Abel Trudno powiedzieć, że coś łapiesz, jeśli nie możesz się do tego odnieść.
Jim Balter
2
@JimBalter Jeśli go nie złapiesz, aplikacja się zawiesza. Jeśli go złapiesz, aplikacja nie ulegnie awarii. Zatem odwoływanie się do obiektu wyjątku różni się od przechwytywania go.
Daniel Earwicker,
Lol! Różnica między zakończeniem aplikacji a kontynuacją to pedanteria? Teraz chyba wszystko słyszałem.
Daniel Earwicker
Co ciekawe, CLR już na to nie pozwala. Domyślnie zawija obiekty niebędące wyjątkami w RuntimeWrappedException .
Jwosty
10

IL ma rozróżnienie między calli callvirtdla wywołań metod wirtualnych. Używając tego pierwszego, możesz wymusić wywołanie metody wirtualnej bieżącego typu klasy statycznej zamiast funkcji wirtualnej w typie klasy dynamicznej.

C # nie ma na to sposobu:

abstract class Foo {
    public void F() {
        Console.WriteLine(ToString()); // Always a virtual call!
    }

    public override string ToString() { System.Diagnostics.Debug.Assert(false); }
};

sealed class Bar : Foo {
    public override string ToString() { return "I'm called!"; }
}

VB, podobnie jak IL, może wywoływać nie wirtualne wywołania, używając MyClass.Method()składni. W powyższym byłoby to MyClass.ToString().

Konrad Rudolph
źródło
9

W trybie try / catch możesz ponownie wprowadzić blok try z jego własnego bloku catch. Więc możesz to zrobić:

.try {
    // ...

  MidTry:
    // ...

    leave.s RestOfMethod
}
catch [mscorlib]System.Exception {
    leave.s MidTry  // branching back into try block!
}

RestOfMethod:
    // ...

AFAIK, nie możesz tego zrobić w C # lub VB

thecoop
źródło
3
Rozumiem, dlaczego zostało to pominięte - Ma wyraźny zapachGOTO
Podstawowy
3
Brzmi jak On Error Resume Next w VB.NET
Thomas Weller
1
W rzeczywistości można to zrobić w VB.NET. Instrukcja GoTo może rozgałęziać się od Catchdo jejTry . Uruchom kod testowy online tutaj .
mbomb007
9

Dzięki IL i VB.NET można dodawać filtry podczas przechwytywania wyjątków, ale C # v3 nie obsługuje tej funkcji.

Ten przykład VB.NET pochodzi z http://blogs.msdn.com/clrteam/archive/2009/02/05/catch-rethrow-and-filters-why-you-should-care.aspx (zwróć uwagę When ShouldCatch(ex) = Truena Klauzula połowowa):

Try
   Foo()
Catch ex As CustomBaseException When ShouldCatch(ex)
   Console.WriteLine("Caught exception!")
End Try
Emanuele Aina
źródło
17
Proszę rusuń = Trueto, moje oczy krwawią!
Konrad Rudolph
Czemu? To jest VB, a nie C #, więc nie ma problemów z = / ==. ;-)
peSHIr
C # może zrobić „rzut”, więc ten sam rezultat można osiągnąć.
Frank Schwieterman
8
PaSHIr, myślę, że mówił o redudancji tego
LegendLength
5
@Frank Schwieterman: Istnieje różnica między łapaniem i ponownym zgłaszaniem wyjątku a wstrzymywaniem się z jego łapaniem. Filtry są uruchamiane przed wszelkimi zagnieżdżonymi instrukcjami „final”, więc okoliczności, które spowodowały wyjątek, będą nadal istniały po uruchomieniu filtru. Jeśli spodziewasz się, że zostanie wyrzucona znaczna liczba SocketException, będziesz chciał złapać stosunkowo cicho, ale kilka z nich będzie sygnalizować problem, bardzo przydatna może być możliwość zbadania stanu, gdy zostanie wyrzucony problematyczny.
supercat
7

Native types
Możesz bezpośrednio pracować z natywnymi typami int i natywnymi unsigned int (w języku C # można pracować tylko na IntPtr, który nie jest taki sam.

Transient Pointers
Możesz grać z przejściowymi wskaźnikami, które są wskaźnikami zarządzanych typów, ale gwarantuje, że nie będą poruszać się w pamięci, ponieważ nie znajdują się na zarządzanej stercie. Nie jestem do końca pewien, jak pożytecznie można go używać bez mieszania się z niezarządzanym kodem, ale nie jest to bezpośrednio ujawniane w innych językach tylko za pośrednictwem rzeczy takich jak stackalloc.

<Module>
możesz zadzierać z klasą, jeśli chcesz (możesz to zrobić przez refleksję bez konieczności posiadania IL)

.emitbyte

15.4.1.1 Dyrektywa .emitbyte MethodBodyItem :: =… | .emitbyte Int32 Ta dyrektywa powoduje emisję 8-bitowej wartości bez znaku bezpośrednio do strumienia CIL metody w miejscu, w którym pojawia się dyrektywa. [Uwaga: Dyrektywa .emitbyte służy do generowania testów. Nie jest to wymagane przy generowaniu zwykłych programów. notatka końcowa]

.entrypoint
Masz w tym trochę większą elastyczność, możesz zastosować to na przykład do metod innych niż Main.

przeczytaj specyfikację Jestem pewien, że znajdziesz jeszcze kilka.

ShuggyCoUk
źródło
+1, tutaj kilka ukrytych klejnotów. Zauważ, że <Module>jest to specjalna klasa dla języków, które akceptują metody globalne (tak jak robi to VB), ale w rzeczywistości C # nie może uzyskać do niej bezpośredniego dostępu.
Abel
Wskaźniki przejściowe wydają się być bardzo użytecznym typem; wiele zastrzeżeń wobec struktur zmiennych wynika z ich pominięcia. Na przykład „DictOfPoints (klucz) .X = 5;” działałoby, gdyby DictOfPoints (klucz) zwrócił przejściowy wskaźnik do struktury, zamiast kopiować strukturę według wartości.
supercat
@supercat, który nie działałby ze wskaźnikiem przejściowym, dane, o których mowa, mogą znajdować się na stercie. chcesz, aby ref zwracał,
ShuggyCoUk
@ShuggyCoUk: Interesujące. Naprawdę mam nadzieję, że uda się przekonać Erica do podania środka, za pomocą którego "DictOfPoints (klucz) .X = 5;" można zmusić do pracy. W tej chwili, jeśli ktoś chce na stałe zakodować DictOfPoints do pracy wyłącznie z typem Point (lub innym określonym typem), można prawie to zrobić, ale jest to uciążliwe. Przy okazji, jedną rzeczą, którą chciałbym zobaczyć, byłby sposób, za pomocą którego można by napisać otwartą funkcję ogólną, np. DoStuff <...> (someParams, ActionByRef <moreParams, ...>, ...), która może się rozszerzać w razie potrzeby. Sądzę, że byłby jakiś sposób na zrobienie tego w MSIL; z pomocą kompilatora ...
supercat,
@ShuggyCoUk: ... zapewniłoby to inny sposób posiadania właściwości przez odniesienie, z dodatkową korzyścią, że niektóre właściwości mogą działać po wykonaniu wszystkiego, co osoby postronne zrobią z odniesieniem.
supercat
6

Możesz zhakować metodę przesłaniającą co / contra-variance, na co C # nie zezwala (to NIE jest to samo, co wariancja ogólna!). Mam więcej informacji na temat implementacji tego tutaj oraz części 1 i 2

miarki
źródło
4

Myślę, że jedynym, o którym marzyłem (z całkowicie niewłaściwych powodów) było dziedziczenie w Enums. Wydaje się, że nie jest to trudne zadanie w SMIL (ponieważ wyliczenia to tylko klasy), ale nie jest to coś, czego wymaga składnia C #.

Shalom Craimer
źródło
4

Oto więcej:

  1. Możesz mieć dodatkowe metody instancji w delegatach.
  2. Delegaci mogą implementować interfejsy.
  3. Możesz mieć statyczne elementy członkowskie w delegatach i interfejsach.
leppie
źródło
3

20) Tablicę bajtów można traktować jako (4x mniejszą) tablicę liczb całkowitych.

Użyłem tego ostatnio do wykonania szybkiej implementacji XOR, ponieważ funkcja CLR xor działa na ints i musiałem zrobić XOR na strumieniu bajtów.

Wynikowy kod był ~ 10x szybszy niż jego odpowiednik w C # (wykonując XOR na każdym bajcie).

===

Nie mam dość stackoverflow street credz, aby edytować pytanie i dodać to do listy jako # 20, jeśli ktoś inny mógłby to być super ;-)

Rosenfield
źródło
3
Zamiast zanurzać się w IL, mógłbyś to osiągnąć za pomocą niebezpiecznych wskaźników. Wyobrażam sobie, że byłby równie szybki, a może i szybszy, ponieważ nie sprawdzałby granic.
P Daddy
3

Coś używanego przez obfuskatorów - możesz mieć pole / metodę / właściwość / zdarzenie, wszystkie mają tę samą nazwę.

Jason Haley
źródło
1
Umieściłem próbkę na mojej stronie: jasonhaley.com/files/NameTestA.zip W tym zipie znajduje się IL i exe, które zawierają klasę z następującymi tymi samymi literami „A”: -nazwa klasy to A -Nazwa zdarzenia A -Metoda o nazwie A -Właściwość o nazwie A -2 Pola o nazwie AI nie mogą znaleźć dobrego odniesienia, na które mógłby cię wskazać, chociaż prawdopodobnie przeczytałem to albo w specyfikacji ecma 335, albo w książce Serge'a Lidina.
Jason Haley
2

Dziedziczenie wyliczenia nie jest naprawdę możliwe:

Możesz dziedziczyć z klasy Enum. Ale wynik nie zachowuje się szczególnie jak Enum. Zachowuje się nawet nie jak typ wartości, ale jak zwykła klasa. Rzecz w tym, że: IsEnum: True, IsValueType: True, IsClass: False

Ale to nie jest szczególnie przydatne (chyba że chcesz zmylić osobę lub samo środowisko wykonawcze).

Zotta
źródło
2

Możesz również wyprowadzić klasę z delegata System.Multicast w IL, ale nie możesz tego zrobić w C #:

// Następująca definicja klasy jest niedozwolona:

klasa publiczna YourCustomDelegate: MulticastDelegate {}

plaureano
źródło
1

Możesz również zdefiniować metody na poziomie modułu (aka globalne) w języku IL, a C # z kolei umożliwia definiowanie metod tylko, o ile są one dołączone do co najmniej jednego typu.

plaureano
źródło