Privatlivets fred: C# vs Python

Siden sidst
Jeg har i den seneste tid arbejdet på et projekt som dykker lidt mere ned i Terraform, Azure, durable functions, Rust og Python. Det stak af i en helt anden retning, da jeg sad og kiggede lidt på forskellen i C# og Pythons sprogets design, og hvordan de, på hver deres måde, hjalp udvikleren med at lave gode snitflader, som andre kan bruge. C# er by design, Python er by convetion. Enkelt og smukt. I samme ombæring begyndte jeg også at kigge på Spacemacs igen, og dertil Omnisharp. Jeg tog Lisp op igen, da jeg gerne vil kunne kalde mit pack script fra Spacemacs. Koden har jeg udgivet her. Jeg er ved at skrive en artikel omkring forskellen på at lave mit pack script, som jeg omtaler her, i
- bash
- python
- powershell
Så ja, min tankerække er som følger: hemmelige projekt –> spacemacs og lisp (ved at lave indlæg) HOV –> python og C#, og et enkelt indlæg (du læser den).
Spacemacs, elisp, bash (og WSL) og Rust er alt sammen noget jeg har dykket mere ned i (eller genoptaget), alene pga et projekt. Et projekt som jeg nok aldrig bliver færdig med, men det gør ikke noget, så længe jeg blot lærer noget nyt. Viden er magt!!
Nok snak - nu kode
Jeg er ved at lave et større skriv omkring tooling, spacemacs, python og elisp, da jeg faldt over noget lettere kuriøs, som jeg egentlig synes er en sjov snak: er private properties egentlig så private, som de udgiver sig for? Du kan starte med at læse med her: https://stackoverflow.com/a/1641236/21199 hvor brugeren Kirk Strauser svarer følgende, til spørgsmålet, om der eksisterer noget som private variabler i Python
It's cultural. In Python, you don't write to other classes' instance or class variables. In Java, nothing prevents you from doing the same if you really want to - after all, you can always edit the source of the class itself to achieve the same effect. Python drops that pretence of security and encourages programmers to be responsible. In practice, this works very nicely.
Det er specielt det sidste, som virkelig fik mine tanker til at flyve
Python drops that pretence of security and encourages programmers to be responsible
Jeg er lige begyndt at kigge mere på Python (en gammel drøm af mine), og jeg må sige jeg elsker det. Python er anderledes, mere kompakt, og mere ligefrem, end andre sprog. Det med at være mere ligefrem (uden dikkedarer og indpakning) kommer derfor ikke kun til udtryk i selve sprogets semantiske opbygning, men også i hele mindsettet og community'et omkring sproget. Specielt når det kommer til private variabler, som er ikke eksisterende i Python:
Use one leading underscore only for non-public methods and instance variables.
Man kan altså som bruger af et API godt kalde, og sætte private variabler /men det gør man, by convetion, IKKE, /og DET SYNES jeg er facinerende:
- det fjerner en masse konstruktioner fra sproget (skal denne variabel være private, protected etc) - og overlader det komplette ansvar til kalderen af API'et
Om overstående er godt eller skidt, vil jeg ikke kloge mig i. Jeg synes det er interessant, for som Kirk skriver, så "lader man ikke som om" at noget er privat i Python, hvilket jeg synes er en interessant tankegang, og jeg må indrømme at jeg er en af de udviklere, som for længst har glemt, at man jo fint, og nogenlunde let kan gøre hvad man vil i .NET, hvis man vil ændre i noget, som egentlig er stemplet som privat. Her ligner tankegangene dog meget hinanden ved .NET og Python udviklere
Man kan godt, men man gør det ikke
I Python rører man ikke ved variabler som er prefixet med en eller to underscores, og i .NET piller man ikke ved det som er angivet som private, selvom det er muligt. Det er dog lidt lettere i Python, end i .NET. Lad mig elaborere.
using System; using System.Reflection;
namespace MyNamespace
{
public class MyClass
{
private int _i = 42;
public int GetI()
{
return _i;
}
}
class Program
{
static void Main(string[] args)
{
var myClass = new MyClass();
var fields = typeof(MyClass).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var field in fields)
{
var val = field.GetValue(myClass);
if (field.IsPrivate)
{
Console.WriteLine($"Private før: {val}");
}
field.SetValue(myClass, 43);
}
Console.WriteLine($"Private efter {myClass.GetI()}");
}
}
}
Det vi ser her er noget som man egentlig ikke må, nemlig at pille ved
private variabler; det er lidt besværligt i C#, da man som sagt skal
bruge reflection, men det kan lade sig gøre!
Vi har ændret variablen, selvom det egentlig ikke burde kunne lade sig
gøre. Hvis man gerne vil undgå at dette sker, kan man lave den private
integer om til en const
, da selv ikke reflection vil kunne ændre det
(man får en fejl når man prøver at køre programmet).
Om MyClass
ligger i samme DLL, eller ej, er sagen ligegyldigt.
Laver vi =_i=om til en static
using System;
using System.Reflection;
namespace MyNamespace
{
public class MyClass
{
private const int _i = 42;
public int GetI()
{
return _i;
}
}
class Program
{
static void Main(string[] args)
{
var myClass = new MyClass();
var fields = typeof(MyClass).GetFields(BindingFlags.NonPublic | BindingFlags.Static);
foreach (var field in fields)
{
var val = field.GetValue(myClass);
if (field.IsPrivate)
{
Console.WriteLine($"Private før: {val}");
}
field.SetValue(myClass, 43);
}
Console.WriteLine($"Private efter {myClass.GetI()}");
}
}
}
Vil vi få en fejl når vi kører programmet, hvor vi prøver at sætte værdien
Husk
at kalde GetFields=med =BindingFlags.Static=i stedet for
=BindingFlags.Instance
, ellers vil =const=feltet ikke blive fundet.
Grunden til overstående er at const=betyder at man på /compile time/
bytter referencen til værdien, =_i
ud med den faktiske værdi. Det viser
sig egentlig fint hvis man decompiler dll'en
Uden
const
vil det decompilede se således ud
Til reference så bruger jeg dnSpy til dekompliering. Den kan også debugge!! Smart.
Men hvad med readonly
? readonly=er en "mellemting" mellem =const=og
en almindelig =private
. Vil en reflektiv =SetValue=på en
=readonly=kaste en fejl, eller vil den gå igennem? Umiddelbart skulle
man tro at en runtime fejl vil blive kastet, da det som sagt kun er
muligt at sætte en =readonly=gennem konstruktøren på en given klasse.
Lad os pille det ad med følgende kode
using System;
using System.Reflection;
namespace MyNamespace
{
public class MyClass
{
private readonly int _i = 42;
public int GetI()
{
return _i;
}
}
class Program
{
static void Main(string[] args)
{
var myClass = new MyClass();
var fields = typeof(MyClass).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var field in fields)
{
var val = field.GetValue(myClass);
if (field.IsPrivate)
{
Console.WriteLine($"Private før: {val}");
}
field.SetValue(myClass, 43);
}
Console.WriteLine($"Private efter {myClass.GetI()}");
}
}
}
Outputtet er drumrole
Det kunne man!! Men hvorfor er det muligt? Kigger man på den decompilede kode ser man følgende
Altså ikke rigtig noget vildt. Der er ikke tilføjet noget ekstra til =GetI=metoden. Decompiler man det til IL
// Token: 0x02000002 RID: 2
.class public auto ansi beforefieldinit MyNamespace.MyClass
extends [System.Runtime]System.Object
{
// Fields
// Token: 0x04000001 RID: 1
.field private initonly int32 _i
// Methods
// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
.method public hidebysig
instance int32 GetI () cil managed
{
// Header Size: 12 bytes
// Code Size: 12 (0xC) bytes
// LocalVarSig Token: 0x11000001 RID: 1
.maxstack 1
.locals init (
[0] int32
)
} // end of method MyClass::GetI
// Token: 0x06000002 RID: 2 RVA: 0x00002068 File Offset: 0x00000268
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Header Size: 1 byte
// Code Size: 16 (0x10) bytes
.maxstack 8
} // end of method MyClass::.ctor
} // end of class MyNamespace.MyClass
Hvor vi kan se … hellere ikke noget specielt, sådan… rigtigt. ANDET
END readonly
bliver oversat til initonly
, som betyder
Specifies that the subsequent array address operation performs no type check at run time, and that it returns a managed pointer whose mutability is restricted.
Fra dokumentationen.
Altså at man på runtime får en pegepind til noget hukommelse som er lidt mere skrap med mutability af det den peger på (rent faktisk er den ikke helt immutable, men den er bare meget restriktiv med hvornår værdien af det den peger på må mutere. EN VÆSENTLIG forskel). Man får ikke en gang lov at bygge kode, hvis =_i=bliver sat udenfor konstruktøren
Den efterfølgende bygge fejl, fortæller det også meget flot
Det var vist en lille afstikker. Konklusionen er som følger:
- Ren
private=kan fint ændres selvom man ikke har en officiel indgang til variablen (fx via en =SetI
) metode const
kan ikke ændres, overhovedet, også selvom man reflecter sig ud af detreadonly
kan omgåes
Vil man være helt sikker på at ens simple værdier ikke ændre sig, er
det en god idé at overveje at bruge const
.
Men hvad med Python, sssscccchhhhiiiiii?
Python er et specielt sprog på mange måder. Den har ingen =const=og sproget ejer hellere ikke skyggen af konceptet omkring private variabler, dog er der meget der er /by convention. /NOgen kalder ligefrem Python for et no bullshit sprog:
- vil du lave noget privat? Prefix din variabel med en eller to underscores. En underscore er faktisk synlig udenfor klassen, men … Lad vær med at ændre den. To underscores bruges faktisk til skærme variabler når der nedarves, dog kan man også bruge den til at fake en privat variabel
- vil du lave noget konstant, skriv den med UPPER CLASS, og lad være med at ændre den
- readonly? Lad være med at ændre variablen andre steder end i klassen konstruktør
Overstående er indforstået i Python: det er en del af designet, og PEP dokumentet, et dokument som fortæller hvordan man koder godt i Python. Overstående er altså by convention, og kræver lidt tilvendig, dog skjuler sproget hellere ikke noget: man kan ændre i private variabler, lige som man så det i C# eksemplerne før (med reflection), men man gør det ikke (lige som man i C# hellere ikke bare ændre private variabler). Man ved man er ude i noget undefined lige så snart man enten ændre i underscore variabler i Python eller i =private=markerede variabler i C#.
Men hvordan ser det ud i Python? Jeg har lavet et eksempel
class MyClass1:
def __init__(self):
self._i = 42
myClass1 = MyClass1()
print(myClass1._i)
class MyClass2:
def __init__(self):
self.__i=42
myClass2 = MyClass2()
print(myClass2.__i); # vil fejle
print(myClass2._MyClass2__i)
myClass2.__i = 43 # virker ikke
myClass2._MyClass2__i = 43
print(myClass2._MyClass2__i)
Eksemplet ovenover viser både enkelt underscore og dobbelt underscore:
- enkelt underscores, kan med lethed kaldes og ændres på en instans af klassen, dog gør man det ikke i Python
- dobbelt underscores, kan med lidt mere møje og besvær, også kaldes. Dobbelt underscore kan ligne at det er Pythons svar på private variabler, dog bruges det til at skjule variabler når man nedarver
Reflektion i Python? I C# er det en kunst i sig selv: man skal refererer de korrekte biblioteker osv, men i Python er det indbygget. Ikke så mange dikkedarer.
Konklusion:
- By design: C# har mange flere håndtag i sproget:
readonly
,private
,const
når det kommer til at definere ens snitflade på API'et, det kræver meget at omgå disse guards, men det kan lade sig gøre - By convention: Python har ikke så mange håndtag i sproget, dog eksisterer de alligevel, da meget er by convention. Det kræver ikke noget særligt at omgå.
Hvis I kunne tænke jer at få at vide hvornår det hemmelige projekt er færdigt, hvad jeg egenligt går og brygger på med spacemacs, python, bash og powershell, så bookmark siden, og følg mig på Twitter, eller LinkedIn.