Nach dem ersten Beitrag zum Thema Attribute habe ich euch ein wenig erklaert um was es ueberhaupt in dieser Serie gehen soll.
Nun wollen wir das auch mal etwas konkretisieren und deshalb dreht sich heute alles darum wie Ihr ein Attribut anwendet und selbst programmiert.
Schauen wir uns als erste mal an, wie man ein Attribut anwendet. Hier hab ich als Beispiel das DebuggerDisplayAttribute rausgesucht. Mit diesem Attribut koennt Ihr bestimmen wie die Formatierung/Ausgabe eines Objektes im Debugging-Modus aussehen soll.
Gehen wir mal von folgender einfachen Klasse aus:
public sealed class Person
{
public string Name { get; private set; }
public int Age { get; private set; }
public Person(string name, int age)
{
this.Name = name;
this.Age = age;
}
}
Wuerden wir nun ein Objekt dieser Klasse erstellen und dieses dann in einem Watch im Debugging untersuchen, so sehen wir als erstes nur folgende Ausgabe.
Das ist das uebliche Verhalten, weil die Laufzeit automatisch die ToString-Methode des Objektes aufruft und wenn diese nicht ueberschrieben ist bekommen wir nur den Namespace -und Klassennamen als Ausgabe.
Moechten man nun aber nicht die ToString-Methode ueberschreiben oder hat sie bereits ueberschrieben moechte sie aber nicht fuer Debugging-Zwecke missbrauchen, so kann man auf das DebuggerDisplayAttribute umsteigen.
Folgende Aenderung ist dafuer an unserer Klasse notwendig.
[DebuggerDisplay("Name = {Name}, Age = {Age}")]
public sealed class Person
{
public string Name { get; private set; }
public int Age { get; private set; }
public Person(string name, int age)
{
this.Name = name;
this.Age = age;
}
}
In der ersten Zeile seht ihr auch schon das ominoese Attribut von dem ich die ganze Zeit geredet habe.
Die Syntax ist eigentlich ziemlich einfach zu merken: Saemtliche Attribute werden in eckigen Klammern geschrieben.
Der Konstruktor(oh ja, auch Attribute koennen Konstruktoren haben, aber das seht Ihr spaeter) des Attributes bekommen einen String welcher die Ausgabe im Debugging darstellt. In den geschweiften Klammern kann auf Properties und Felder der Klasse zugreifen.
So und wenn nun alles geklappt hat gehen wir nochmal in den Debugging-Modus und sehen das es so aussieht wie wir uns das vorgestellt haben:
Ziemlich cool oder?
Natuerlich koennt Ihr auch noch die Properties und Felder der Klasse mit speziellen DebuggerDisplay Attributen versehen, so erhaltet Ihr dann im Debugging eine extra Formatierung fuer diese Felder.
Ok, soviel dazu. Nun wirds ernst, wollen wir mal ein eigenes Attribut entwickeln.
Manchmal gibt es ja Momente wo man mal ein Enum schreibt. Wir denken uns einen Namen fuer das Enum aus, die passenden Felder werden erstellt und wir benutzen es und dann…. passiert es. Wir brauchen im Code zur Unterscheidung gewisser Fakten die Englischen Begriffe die wir uns im Enum ausgedacht haben aber fuer einen anderen Zweck moechten wir jedem Enum-Wert noch einen anderen Namen zuordnen. Nun faengt das Dilema an – benenn ich das Enum um? Mach ich Killer-Switch? Oder oder oder…
Bis man merkt: “Hey, das ist doch der perfekte Moment um mir ein Attribut zu bauen.”
Genauuuuuuuu!!
Und hier ist das Attribut in seiner vollen Pracht.
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class AlternativeStringAttribute : Attribute
{
public string AlternativeString { get; private set; }
public AlternativeStringAttribute(string alternativeName)
{
this.AlternativeString = alternativeName;
}
}
Was man hier sofort erkennt ist das unser Attribut wiederum Attribute benutzt – was fuer ein Teufelskreislauf.
Aber das Attribut was wir hier benutzen ist dazu da um unser neues zu Konfigurieren. Wir geben hier an wo wir unser Attribut spaeter mal verwenden wollen und ob es mehrfach an einer dieser Stellen benutzt werden darf. In diesem Fall soll das Attribut nur auf Felder angewendet und nicht mehrfach an einem Feld benutzt werden.
Der naechste Punkt der sehr wichtig ist, wir muessen UNBEDINGT von Attribute erben. Das ist die Basisklasse fuer alle Attribute die Ihr mal schreiben werdet.
Als letzte bekommt das Attribut noch eine Property in dem der alternative Name des Enums gespeichert werden soll.
Nun haben wir ein Attribut geschrieben und wollen es natuerlich auch anwenden, dazu geb ich euch folgendes simples Enum.
public enum Features
{
[AlternativeString("Neuheiten")]
New,
[AlternativeString("Altes Zeug")]
Old,
[AlternativeString("Updates")]
Updated,
Tests
}
Gut nun haben wir es verwendet, aber jetzt soll das ganze ja auch irgendwie ausgewerte werden.
Am einfachsten gestaltet sich da eine kleine Helfer-Klasse.
public static class EnumHelper
{
public static string AlternativeValue(this Enum enumValue)
{
var type = enumValue.GetType();
var fieldInfo = type.GetField(enumValue.ToString());
if (fieldInfo != null)
{
return GetAlternativeValue(fieldInfo);
}
return enumValue.ToString();
}
private static string GetAlternativeValue(FieldInfo fieldInfo)
{
var attribute = fieldInfo.GetCustomAttributes(typeof(AlternativeStringAttribute),false) as AlternativeStringAttribute[];
if (attribute != null && attribute.Length > 0)
{
return attribute[0].AlternativeString;
}
return fieldInfo.Name;
}
}
Und hier kommt auch die bereits angekuendigt Reflection ins Spiel. Bitte seid euch bewusst das Reflection zwar ein maechtiges Tool ist aber auch schnell sehr unuebersichtlich werden kann wenn man keinen klaren Coden schreibt.
Was ich hier gemacht habe ist folgendes: Als erstes habe ich mir eine Extension-Method geschrieben um den allgemein Typ Enum zu erweitern. Innerhalb dieser Methode hole ich mir den aktuellen Typ des uebergeben Enums und hole mir ein bestimmtes Feld, naemlich jenes welches ich als Parameter uebergeben bekomme.
Von diesem Feld hole ich mir alle CustomAttributes vom Typ AlternativeStringAttribute. Wenn ich hier ein Ergebnis bekomme so nehme ich mir nur das erste Element, weil mehr nicht kommen koennen, und gebe von diesem den AlternativenString zurueck.
Sollte ich keine Attribute bekommen, geb ich einfach den Feldnamen des Feldes zurueck.
Als letztes noch ein kleines Codestueck wie es dann in der endgueltigen Verwendung ausschaut.
public sealed class Program
{
public static void Main(string[] args)
{
Features f = Features.Old;
Console.WriteLine(f.AlternativeValue());
}
}
Die Ausgabe ist wie erwartet: “Altes Zeug”
Bis dann euer
ckruczek
Da ich selbst mit diesem Thema lange Zeit zu kämpfen hatte als ich mit C# angefangen habe, dachte ich mir das es sich lohnt darüber mal einen Eintrag zu verfassen. Mich haben immer drei Fragen beschäftigt:
1. Was sind Attribute?
2. Wie funktionieren sie?
und
3. Wo und wann wende ich sie an?
Ich würde sagen wir fangen langsam an und gehen in diesem Post nur die erste Frage an, weil ich ungerne überlange Posts möchte wo ich alles abfackel. Soll also heißen das es wieder eine Serie von Posts zu diesem Thema gibt.
1. Was sind Attribute?
Um es kurz zu halten: Attribute bieten die Möglichkeit Klassen, Methoden, Properties, Felder, Konstruktoren und weitere, siehe dazu folgende Tabelle, zu markieren um diese zur Laufzeit auszuwerten und Benutzerdefinierten Code auszuführen.
Mit Attributen kann man also selbst Klassen schreiben welche Code beinhalten der dann ausgeführt wird wenn man die Attribute an einer Klasse auswertet. Jetzt fragt man sich natürlich berechtigter Weise: “Wenn die Auswertung nur zur Laufzeit möglich ist, heisst das dann das wir hier Reflection benutzen müssen?”. Jeder der sich diese Frage jetzt gestellt hat ist auf dem richtigen Weg die Attribute richtig zu verstehen, alle anderen werden hier jetzt in die Materie eingeführt und sind danach hoffentlich aufgeklärter in diesem Thema.
Um auf die Reflection-Frage zurück zukommen: Ja, wir brauchen hier Reflection und müssen davon auch imens viel Gebrauch machen. Natürlich darf man jetzt sagen das dass doch gefährlich ist und man dadurch viel Kontrolle verliert. Auch diese Aussage ist korrekt, aber hier soll es ja darum gehen die Reflection so zu nutzen das man damit sicherer wird und im Umkehrschluss auch damit sicheren Code schreiben kann.
So damit soll es für den ersten Eintrag in dieser Serie erstmal reichen. Ich hoffe das genügt als kurze Einleitung zum dem um was hier gehen soll und ihr seid gespannt auf den Rest.
Bis dann euer
ckruczek
Wie im 2. Teil bereits angekündigt, werden wir uns diesmal mit dem Thema Schleifen auseinandersetzen.
Möchten wir eine einfache for-Schleife mit einem Anfangs -und Endwert sowie einem benutzerdefinierten Schleifenkörper erstellen, sollten wir uns mal Gedanken darüber machen aus was so eine for-Schleife überhaupt besteht.
Nehmen wir doch folgenden Codeausschnitt mal auseinander und transformieren ihn in einen Expression-Tree.
int[] numbers = Enumerable.Range(1,10).ToArray(); for(int i = 0; i < numbers.Length; i++) Console.WriteLine(numbers[i]);
Den Teil wo ich das Array erstelle zählt nicht mit in die Schleifenfunktion die wir gleich schreiben werden.
Jedoch erstmal eine Analyse, was wir brauchen:
1. Eine Variable welche die Startbedingung definiert (from-Condition).
2. Eine Variable welche unsere Endbedingung definiert (to-Condition).
3. Einen Block mit dem wir die eigentliche Schleife simulieren.
4. Eine If-Abfrage mit welcher wir überprüfen ob unsere “from-Condition” bereits die “to-Condition” erreicht hat.
5. Eine Erhöhung unserer Zählervariable.
6. Eine Funktion welche unseren benutzerdefinierten Schleifenkörper darstellt.
So viel zur Theorie, jetzt die Praxis
private static Expression SimpleForLoop(int from, int to, Expression<Action<int>> loopBodyAction)
{
Type tType = typeof(Int32);
// 1.
ParameterExpression fromExpression = Expression.Variable(tType, "fromCounter");
// 2.
ParameterExpression toExpression = Expression.Variable(tType, "toCounter");
LabelTarget end = Expression.Label();
Expression loop = Expression.Block
(
new ParameterExpression[] { fromExpression, toExpression },
Expression.Assign(fromExpression, Expression.Constant(from, tType)),
Expression.Assign(toExpression, Expression.Constant(to, tType)),
// 3.
Expression.Loop
(
// 4.
Expression.IfThenElse
(
Expression.LessThan(fromExpression, toExpression),
// True Path
Expression.Block
(
// 6.
Expression.Invoke(loopBodyAction, fromExpression),
// 5.
Expression.PostIncrementAssign(fromExpression)
),
// False Path
Expression.Break(end)
), end
)
);
return loop;
}
Joa das ist diesmal nen gewaltiger Batzen Code, aber ihr kennt ja das Prozedere.
Im ersten Teil der Funktion deklarier ich erstmal nur zwei Parameter, nämlich jene welche wir als from- und to-Condition später benutzen. Da man in der Welt der Expression-Trees keine for-Schleifen kennt, müssen wir unsere Schleife also händisch abbrechen. Deshalb erzeuge ich im nächsten Schritt ein Label an welches wir später mit einem Break springen können.
Dann kommt eine normale Block-Expression gefolgt von der Deklaration der Parameter, welche wir innerhalb des Blockes benutzen wollen. Über den Befehl Expression.Assign weisen wir der neu erzeugten fromExpression den Wert der from-Variable zu. Genauso machen wie wir es mit der toExpression und der to-Variable.
Als nächstes kommt unser eigentlicher Loop Body. Damit unsere Schleife auch irgendwann beendet wird, müssen wir eine einfache If-Abfrage durchführen, in welcher wir schauen, ob die fromExpression noch kleiner ist als die toExpression. Ist dem so, führen wir die übergebene Funktion aus, welcher wir den aktuellen Zählerstand mitgeben. Danach erhöhen wir noch den Zähler und sind fertig mit dem Teil, wenn die fromExpression noch kleiner ist als die toExpression. Nun soll es ja vorkommen das die If-Abfrage in geraumer Zeit einmal false zurück liefert. Auch darauf müssen wir reagieren. Das machen wir mit ein einer einfachen Break-Anweisung, welche man der Loop-Funktion übergeben kann. Diese Stelle wird dann angesprungen und die Schleife beendet. Damit wir diese Schleife etwas modularer einsetzen können geben wir den Loop-Block zurück und können diesen beim Aufruf der SimpleForLoop-Funktion weiter benutzen.
Jetzt stellt sich der ein oder andere sicherlich die Frage wie man das ganze nun aufruft.
Dann will ich euch mal nicht auf die Folter spannen und einen Beispielaufruf demonstrieren.
public static void Main(string[] args)
{
int[] numbers = Enumerable.Range(1, 10).ToArray();
Expression loopExpression = SimpleForLoop(0, numbers.Length, a => Console.WriteLine(numbers[a]));
Expression.Lambda(loopExpression).Compile().DynamicInvoke(null);
Console.ReadLine();
}
Wir erstellen uns ein Array mit 10 Zahlen, ich denke das sollte ersichtlich sein
Danach rufen wir unsere Funktion auf indem wir sagen:”Liebe Funktion, du sollst eine for-Schleife von 0 – DieGroesseDesArrays – 1 erstellen. Bei jedem Schleifendurchlauf benutze die Funktion Console.WriteLine und gib den Wert des Arrays am aktuellen Index aus.”
Soweit klar oder?
Ich würde gerne mal eure Meinung hören ob die Themen die ich hier bearbeite zu komplex sind, oder ob ich die Posts nicht ausführlich genug halte. Man kann auch gerne mit Themen-Wünschen an mich heran treten. Schreibt mir einfach mal eine Mail mit euren Meinungen. Ich freu mich.
Bis bald euer
ckruczek