Kuvatud on postitused sildiga c#. Kuva kõik postitused
Kuvatud on postitused sildiga c#. Kuva kõik postitused

Sellest, kuidas parsida C# koodifaile..

Laisk inimene ei tee käsitööd vaid kulutab (halvemal juhul sama suurusjärku) aja loomaks vahendeid, mis nüri töö tema eest ära teeks. Mul oli vaja iga koodifaili kohta teada, mis nimeruume ta deklareerib ja millised klasse ta sisaldab. Failide arv on paari tuhande ringis, seega käsitsi üle käimine ei olnud kindlasti plaanis.

Regexp ?

Esimene mõte oli vanad head regulaaravaldised. Nende lugemine ja debugimine on küll tükk vaeva, aga päris palju saab ära teha. Lihtsustatud stsenaariumi nimeruumide leidmine on mõõdukal lihtne : need on faili alguses, enne deklaratsiooni on üldjuhul vaid using-käsud ja erinevad kommentaarid, võib-olla ka hunnik whitespace’i. Aga sisemised nimeruumid ? mitu nimeruumi failis?  atribuudid ? Klasside leidmine oleks sel meetodil veel kordades keerukam, sundides arvestama ka näiteks string-konstantides sisalduvaga jms..

Või parser ?

Ja siis taipasin – nii tore kui jalgratta leiutamine ja regulaaravaldiste kirjutamine ka ei ole, antud probleem on ju juba ammu lahendatud. Iga C# koodiparser teeb oma põhitöö seas just seda, mida mul vaja. Näiteks proovisin SharpDevelop’i (vabavarane IDE c# jm jaoks) koosseisu kuuluvat NRefactory-nimelist parserit.

Samm1: muretse NRefactory

NRefactory näib olevat kättesaadava ainult SharpDevelop koosseisus, mille installeri  saab siit: http://www.icsharpcode.net/OpenSource/SD/Download/, misjärel on teek leitav sõltuvalt installeerimise kohast, näiteks siit:

C:\Program Files (x86)\SharpDevelop\4.0\bin\ICSharpCode.NRefactory.dll

Kui sa ei taha SharpDevelopiga tutvuda, siis dll saab loomulikult ka SharpDevelopi lähtekoodist.

Samm2: Kood

using ICSharpCode.NRefactory;
using ICSharpCode.NRefactory.Ast;

Parseri loomisel ei ole just palju valikuid, kuid mõned siiski. kuna meid huvitavad ainult klassid-nimeruumid, siis sobib näiteks selline parseri loomine.

IParser parser = ParserFactory.CreateParser(@"C:\Somepath\somecodefile.cs");
parser.ParseMethodBodies = false;
parser.Parse();

if (parser.Errors.Count > 0)
{
throw new Exception(String.Format("Parsing of '{0}' failed, Errors: {1}.",
codeFile.FullName, parser.Errors.ErrorOutput));
}

// this is my method to query the information.
WalkFile(parser.CompilationUnit.Children);


Meetod WalkFile() käib rekursiivselt läbi AST (Abstract Syntax Tree) ja viskab mind huvitava informatsiooni konsooli. Loomulikult oleks kena rekursiooni vältida, aga see ei ole praegu oluline:

private static void WalkFile(List<INode> nodes)
{
foreach (var node in nodes)
{
var asNamespace = node as NamespaceDeclaration;
if (asNamespace != null)
{
Console.WriteLine("Namespace found: '{0}' from {1}",
asNamespace.Name, asNamespace.StartLocation.ToString());
}

var asType = node as TypeDeclaration;
if (asType != null) {
Console.WriteLine("Type found: '{0}' from {1}",
asType.Name, asType.StartLocation.ToString());
}
WalkFile(node.Children);
}
}

Ja tulemuseks näiteks midagi sarnast:

Namespace found: 'SomeRoot.Parent' from (Line 6, Col 1)
Type found: 'ClassInParent' from (Line 8, Col 2)
Type found: 'SubClass' from (Line 10, Col 3)
Namespace found: 'ChildNamespace' from (Line 23, Col 2)
Type found: 'ClassInChild' from (Line 25, Col 3)

Mõistmaks, mis klass-nimeruum, mille sees asub, peaks muidugi vanemate ahelat vms infot meeles pidama, aga see ei ole enam midagi keerulist.

mis edasi ?

Koodifailide parsimine võimaldab teha koodi pealt päris huvitavaid päringuid. Näiteks kontrollida:

  • kas klassid jms ikka kasutavad kokkulepitud nimetamisreegleid ?
  • millised klassid ei järgi kokkulepitud public/internal/private-reegleid?
  • millised koodifailid ei järgi “1 file = 1 class” põhimõtet ?
  • millised koodifailid ei asu õiges kaustas (näiteks nimeruumi järgi) ?
  • mitu rida on pikim meetod ?

Mitte kõike neist ei saa teha reflectioni abil ja erinevalt reflectionist ei ole sul vaja kompileerimist, vaid piisab märgatavalt vähematest eeldustest: ainult vaadatavad koodifailid peab olema süntaktiliselt korrektsed. Puuduvad teegid, või kompileerimist takistavad vead ei takista teisi faile analüüsimast. Või suvalist faili.

Võimalik on teha näiteks koodifailide konverteerijat, mis näiteks eemaldab või lisab atribuute, muudab klassivälju, näiteks field –> property jne. Üks väike video sel teemal on näiteks siin. AST abil koodifailide muutmisel on küll miinuseks see, et kommentaarid lähevad kaotsi, mistõttu on ehk kaval kasutada parsimist informatsiooni pärimisks ja kasutada parseri antud asukohainfot automaatsete muudatuste tegemiseks muid vahendeid kasutades.

About optional parameters in c# when targeting .net 2.0

One of the coolest thing about Visual Studio 2010 is C# 4.0 and its optional/named parameters feature. I was wondering if I can get rid of those hard to maintain overloads in my older assemblies which still need to target the CLR2.

Just to be sure I made a quick proof of concept:

Code for test program

   1: using System;
   2:  
   3: namespace OptionalParameterTest
   4: {
   5:     class Program
   6:     {
   7:         static void Main(string[] args)
   8:         {
   9:             TestMethod();
  10:             TestMethod("only string set");
  11:             TestMethod("both set", 12345);
  12:             TestMethod(someint: 12345, someString: "named parameters, both set");
  13:             TestMethod(someString: "named parameters, only string set");
  14:         }
  15:  
  16:         private static void TestMethod(String someString = "defaultStringValue", int someint = 9)
  17:         {
  18:             Console.WriteLine("Called with: " + someString + " " + someint);
  19:         }
  20:     }
  21: }

This is ofc expected to result with such output:

   1: C:\WM\Varia\OptionalParameterTest\bin\Debug>OptionalParameterTest.exe
   2: Called with: defaultStringValue 9
   3: Called with: only string set 9
   4: Called with: both set 12345
   5: Called with: named parameters, both set 12345
   6: Called with: named parameters, only string set 9

So, the target FW is 4.0

Compile, run, works as expected. Corflags agree that the executable indeed requires version 4:

   1: Version   : v4.0.30319
   2: CLR Header: 2.5
   3: PE        : PE32
   4: CorFlags  : 3
   5: ILONLY    : 1
   6: 32BIT     : 1
   7: Signed    : 0

Lets note that we have such line in app.config added by VS itself:

   1: <startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup>

When target FW is 2.0

Changed target fw of the VS project, compile, run.. result was as expected and corflags agrees that we now require older CLR:

   1: Version   : v2.0.50727
   2: CLR Header: 2.5
   3: PE        : PE32
   4: CorFlags  : 3
   5: ILONLY    : 1
   6: 32BIT     : 1
   7: Signed    : 0

Btw, VS automatically adjusted my app.config to require correct version as shown below. Nice touch!

   1: <startup><supportedRuntime version="v2.0.50727"/></startup></configuration>

But lets be paranoid..

.. and really make sure that the correct runtime version corflags reports was used when running the application. For this I deployed both solutions to an box without .NET 4, just .NET 3.5.

  • Assembly targeted against CLR2.0 ran just fine. (This is what makes me happy!)
  • As expected the assembly targeted against CLR4.0 refused to run stating quite descriptevely that it needed .Net FW 4.0.

Note:

When app.config lies abiout it’s required runtime version, like stating CLR 2.0 instead of 4.0 then you get much uglier and much less descriptive “crash, debug?”-message. Be careful about that and do use the supported runtime  version setting in configuration file instead of just assuming there will be the right one present and used.

Conclusion

Optional and named parameters are C# compiler-level features. Which means the runtime does not care if you use them or not – compiler is just better at understanding which method’s call to use and inserts parameter initialization where neccessary. The generated MSIL is still CLR 2.0. The compiler just helps us to achieve more by writing less. One more reason to switch to VS2010 even when still targeting older frameworks for whatever reasons. Yei!

Sellest, et WCF proxy saab using-blokiga halvasti läbi..

On saanud harjumuseks IDisposable tüüpe kasutades kohmaka try-finally bloki asemel ilusat using-süntaksit kasutada. Paraku selgus, et Windows Communication Foundation ei ole selle suhtes väga sõbralikult meelestatud juhul kui teenuse tarbimise käigus tekib mõni viga WCF kanaliga. Näiteks:

using (var usingClient = new FtpProxyServiceClient())
{
    usingClient.RecieveFile(target); //this call throws an exception
}

tagastab võrdlemisi kasutu veateate:

System.ServiceModel.CommunicationObjectFaultedException: The communication object, System.ServiceModel.Channels.ServiceChannel, cannot be used for communication because it is in the Faulted state.

Server stack trace:
   at System.ServiceModel.Channels.CommunicationObject.Close(TimeSpan timeout)

Exception rethrown at [0]:
   at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
   at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
   at System.ServiceModel.ICommunicationObject.Close(TimeSpan timeout)
   at System.ServiceModel.ClientBase`1.System.ServiceModel.ICommunicationObject.Close(TimeSpan timeout)
   at System.ServiceModel.ClientBase`1.Close()
   at System.ServiceModel.ClientBase`1.System.IDisposable.Dispose()
   at SomeNameSpace.UIConsole.Program.Test() in W:\SomePath\UIConsole\Program.cs:line 32

Pöörakem tähelepanu, et vea põhjuse või tekkekoha kohta ei ole jälgegi, vaid viga näib tulevat hoopis Dispose() väljakutsumisel. Selgub, et WCF proxy Close() ebaõnnestub kui eelneva vea tõttu on ühendus juba maha võetud. Tulemuseks on UUS viga ja vana unustatakse hoopis.

Lahenduseks on tagasipöördumine try-finally lahenduse poole ja Abort() kasutamine:

var tryClient = new FtpProxyServiceClient();
Boolean succeeded = false;
try
{
    tryClient.RecieveFile(target); //this call throws an exception
    tryClient.Close();
    succeeded = true;
}
finally
{
    if (!succeeded) { tryClient.Abort(); }
}

Ehk vea järel kutsume Close() asemel välja Abort() meetodi. Selle peale saame lõpuks ometi teada ka algse vea põhjuse, asjaosalise stackframe'i jne:

System.ServiceModel.CommunicationException: The maximum message size quota for incoming messages (20) has been exceeded. To increase the quota, use the MaxReceivedMessageSize property on the appropriate binding element. ---> System.ServiceModel.QuotaExceededException: The maximum message size quota for incoming messages (20) has been exceeded. To increase the quota, use the MaxReceivedMessageSize property on the appropriate binding element.
   --- End of inner exception stack trace ---

[ - skipped some nonrelevant stacktrace entries - ]

   at SomeNameSpace.Ftp.IFtpProxyService.RecieveFile(Uri targetFile)
   at SomeNameSpace.Ftp.FtpProxyServiceClient.RecieveFile(Uri targetFile) in w:\SomePath\ServiceClients.Ftp\Reference.cs:line 102
   at SomeNameSpace.UIConsole.Program.Test()
                 

Kui jõudlus ei ole probleemiks siis funktsionaalselt enam-vähem samaväärne on õnnestumise jälgimine ära jätta ja Abort() alati välja kutsuda. Viga ta ei viska, aga lisandub väike performance overhead ja see oleks ka lihtsalt vale ;)

Abiks oli Damian McGivern'i postitus, kus muuhulgas vihjatakse, kuidas seda try-finally-succeeded blokki mugavamaks teha saaks.

Huvitav, kas c# 4.0 jaoks on WCF proxy Close() pisut koostööaltimaks tehtud ..

Sellest, mida System.Decimal kõhus peidab..

Ilmselt iga asjalikum koodikirjutaja teab, mille poolest erinevad System.Single (või float, kui keegi seda rohkem eelistab) ja System.Decimal andmetüübid. Yada-yada, kahendsüsteem vs kümnendsüsteem. Sellegipoolest jäin mõneks hetkeks mõttesse kui avastasin, et ka ToString() käitub erinevalt:

Console.WriteLine((1.0F).ToString());   // returns '1'
Console.WriteLine((1.000F).ToString()); // returns '1'

Console.WriteLine((1M).ToString());     // returns '1'
Console.WriteLine((1.000M).ToString()); // returns '1,000'
Console.WriteLine(1M == 1.000M);        // returns true

Järeldus tuleviku tarbeks on see, et Decimal korral ei tohi eeldada, et ToString() sama väärtusega sisendi korral alati sama vastuse annaks. Kui formaat on oluline, siis tuleb alati see ka täpsustada ning mitte lootma jääda parameetriteta meetodile.

Console.WriteLine((1.000M).ToString("0"));   
// returns '1'

Loomulikult, kui formaat on märgi täpsusega oluline, siis tuleks ka kultuur täpsustada. Antud juhul ei ole see oluline.

 

Miks Decimal nii käitub ?

Teatavasti on  Decimal 128-bitine struktuur. Sisemiselt:

  • 3 * 32bit "täpsusosa" - low, med and  hi-bits
  • märgibitt
  • scale-väärtusele, mis määrab kui suur osa täpsusosa kümnendkohtadest on murdosa.

Siit ka tuleneb lubatud väärtuste hulk:

{ s * c * 10^(-e)|
     s kuulub hulka {-1,1}, 
     0 <= c <= 2^96 ,
     ja 0 <= e <= 28
}

Muuhulgas nähtub, et seetõttu leiduvad erinevad (s,c,e)-komplektid (ehk decimal sisemised väärtustused), mis omavad muutuja kasutaja poolt vaadatuna sama väärtust (näiteks arvu 1). Eelpool kirjeldatud näide demonstreeris just kahte erinevat sisemist väärtustust.

Illustreerimiseks võib selle näite veel ilmekamalt lahti kirjutada, kasutades konstruktorit, mis võimaldab kõiki eeltoodud komponente ise sisestada:

Decimal d10 = new decimal(10, 0, 0, false, 1);
Console.WriteLine(d10.ToString());   // returns '1,0'
Decimal d1000 = new decimal(1000, 0, 0, false, 3);
Console.WriteLine(d1000.ToString()); // returns '1,000'
Console.WriteLine(d10 == d1000);     //returns True;


Aga mille jaoks on ülejäänud bitid ?

Komponendid e ja märgibitt saavad kasutada 128 - 96 = 32 biti jagu ruumi. Kui märgibitt võtab neist tubli 1, siis miks e lubatud väärtused on piiratud 29 erineva väärtusega, kui kasutada on 2^31 ? Vägisi jääb mulje, et 31 - up(log_2 29) = 26 bitti iga Decimali kohta on kasutu ballast.. Raske uskuda, aga mõistlikku selgitust ei näe.


Aga miks System.Single ToString() ikkagi teisiti käitub ?

Teoreetiliselt on ka nende jaoks võimalik konstrueerida samasugune juhtum (1 * 2^10 vs 2 * 2^9) . Pead või sõrmi panti ei paneks, aga oletan, et levinumate floating-point tüüpide korral lahendatakse "normaliseerimine" riistvaras. Kahendsüsteemis nihutamine on ka oluliselt  odavam ettevõtmine.

Sellest, et nimetamise head tavad on koondunud..

Järjekordne C# koodielementide nimetamise mitteametlik standard: http://www.irritatedvowel.com/Programming/Standards.aspx
Lühidalt, ülevaatlikult, näidetega.

Kui ux-prefix kõrvale jätta, siis ma olen kõigega nõus. Isegi underscore prefixiga (mis mind varem häiris).