ZitatOriginally posted by Dieter
Hi,
ja diese Buch ist gut (Autor ist Mayer?)
Hight Higth verwechsele ich seit Jahren. Same with chnage and change.
Height , please.
So wie es aus dem Wald schallt , so...
ZitatOriginally posted by Dieter
Hi,
ja diese Buch ist gut (Autor ist Mayer?)
Hight Higth verwechsele ich seit Jahren. Same with chnage and change.
Height , please.
So wie es aus dem Wald schallt , so...
Meine Meinung (sorry): unnötig und Zeitverschwendung.
Es werden sich 75% der Leute nicht dran halten, weil
a) Sie begründete Einwände gegen _einen_ Guide haben
b) Sie ihre eigene Notation seit 200 Jahren verwenden und keine andere nimmermehr
c) Sie nich wissen, was sowas überhaupt is und einfach coden.
Und wer soll letztenendes sowas festlegen ausser Klaus?
Was mir dann nötiger scheint, sind inline-Doku und Interface-Beschreibungen.
Von gewissen Design-Ideen mal abgesehen ...
Ich weiss: Mein Code is megakwark - keine Frage - aber am Anfang einer Entwicklung hab ich andere Sorgen als das der Code lesbar ist ;).
In diesem Sinne - ned böse sein
arghgra
Hallo,
eine loose Samlung ist für den Anfang sicher gut. Danach kann man das immer noch zusammenfassen.
Wenn wir das gemeinsame machen, wird das sicher was, da dann die Anfänger ihre Fragen stellen die man dann ev. einarbeiten kann.
PS: So wie LordJaxom mache ich (und viele andere) das auch mit der unsäglichen Notation.
Ein aktueller C/C++ Compiler braucht sowas nicht. Es wurde erfunden damit der Mensch den Überblick über die Typen behält - heute macht das der Compiler und schimpf wenn es nicht passt.
Ich arbeite übrigens oft nur mit Editor (Multi-Edit) und make/compiler etc.
Was ich anbieten kann ist ein C++ für C Programmierer (ca. 90KB PDF) in English. Habe ich mal für meine Kollegen in den USA geschrieben.
LordJaxom: Wird am burn pre1 eigentlich noch gearbeitet? Hätte da ein paar Ideen, die ich einbauen könnte. Gibts ein changelog?
Wenn man sich als Plugin-Einsteiger mehr mit dem, was man eigentlich will beschäftigen könnte, nämlich den 3 1/2 Kern-Code-Zeilen, die einem vorschweben, dann wären die Codes wahrscheinlich 'von alleine' sauberer. Aber bis die 'Entwicklungsumgebung' steht, ist bei den meisten wohl schon 99-100% der Energie verballert...
Ich denke, was Plugin-Anfängern wirklich helfen würde, wäre eine erheblich ausgebaute Dokumentation der Schnittstellen. Die PLUGINS.html gibt kaum mehr als einen groben Überblick, die Headerdateien sind nur zum Teil dokumentiert, die Quelltexte noch weniger. Letztlich muss man als Plugin-Autor bei anderen Plugins abkupfern oder die VDR-Quelltexte intensiv studieren - was bei Klaus' Stil manchmal nicht einfach ist.
Und zum ursprünglichen Thema:
Der Programmierstil ist letztlich Geschmackssache. Da muss jeder seinen eigenen Weg finden. Und wenn der Quelltext auch nach ein paar Tagen noch auf den ersten Blick leicht zu verstehen ist, kann es nicht ganz falsch sein.
Gruß,
Udo
Ich denke wenn jmd überhaupt ne Style Anleitung macht es keinen Sinn, die Grundlagen zu wiederholen, besser wärs an kleinen Beispielen Prinzipien und Vorgehensweisen zu sauberem Code darzulegen. Stichpunkte mit Beispiel würden schon reichen..
ZitatOriginal von wirbel
Ich denke wenn jmd überhaupt ne Style Anleitung macht es keinen Sinn, die Grundlagen zu wiederholen, besser wärs an kleinen Beispielen Prinzipien und Vorgehensweisen zu sauberem Code darzulegen. Stichpunkte mit Beispiel würden schon reichen..
Könnte es sein , daß jeder , der über Spaggetticode hinaus gekommen ist , weiß , wie er seine Sourcen nachvollziehbar strukturieren und kommentieren kann ?
HJS
ZitatOriginally posted by Urig
...oder die VDR-Quelltexte intensiv studieren - was bei Klaus' Stil manchmal nicht einfach ist.
Woran scheitert's denn?
Klaus
Hallo,
Jeder der Software schreibt, weiss dass es hin und wieder schmutzige Stellen gibt. Auch bei mir! Dies zu säubern ist die Aufgabe von uns allen die hier Programmieren. Und C/C++ ist tausendmal sauberer as die meisten Scripte.
Klaus hat mal angefangen mit doxygen, aber man findet noch nicht sehr viel davon. Ich habe für meine Editoren (Multi-Edit und MSVC 7 und Macros gemacht. Damit ist es sehr einfach die speziellen doxygen Kommentare einzufügen. Seither füge ich viel mehr Kommentare hinzu. Es muss halt bequem sein, sonst ist man zu faul...
Mein erster Beitrag zielte primär nicht auf Kommentare, sondern auf "selbstdokumentierenden Code". Dies war das Buzzwort vor ca. 20 Jahren (Ja ich bin recht alt an Jahren). Ich halte nicht viel von Modeströmungen in diesem Gebiet, aber dies war im Ansatz eine gute Idee und ich setzte dies auch (fast) immer ein.
Ich wäre fürs erste schon mal zufrieden wenn die nackten Zahlen durch sprechende Namen ersetzt werden. Und beim Declarieren kurz geschrieben wird was drin ist in der Variablen.
Beispiel aus einem USB Treiber (mit doxygen). Leider mag die forensoftware meine Tabs nicht so gerne. Man sieht auch das Bischen unsägliche Notation das ich Verwende.
// Setup members
CUsbChip *m_pChip; //!< Used to call STALL and ACK functions
UCHAR *m_pUsbCmd; //!< Point to the 8 byte USB command buffer
CEndpointFunctor *m_pGotSetupAction; //!< "Functionpointer" for callback after a Setup request has been received.
// Common data members
CThreadSimple m_ThreadSetupRequest; //!< Thread handler for operations slower than 10uSec
.....
///////////////////////////////////////////////////////////////////////////////
/// Handler for a USB endpoint.
/*! This class trys to be as hardware independend as possible, but it cannot
guarantied that it is really hardware independend.
There is one object for each endpoint.
*/
class CUsbEndpoint
{
.....
const UCHAR *m_pInPointer; //!< Point to data that still needs to be send
int m_RestSizeIn; //!< Amount of data that still needs to be send
int m_InPacketSize; //!< Size of one USB packet (8,16,32,64)
size_t m_SizeInTotal; //!< Total amount of data in this transaction
....
Alles anzeigen
Gestern habe ich versucht die records.c zu verstehen. Probleme hatte ich zum Beispiel herauszufinden was in der Variablen FileName drin ist. Ist es /video/Film/2006.03.31.50.50.rec oder nur ein Teil davon. Kommentare wie in meinem Beispiel hätten schon geholfen.
@Klaus: Bitte nicht falsch verstehen, ich möcht absolut nicht meckern.
Wenn Du willst, kann ich bei meinen Experimenten passende Kommentare hinzufügen und dir schicken. Solltest sie aber gut prüfen damit kein Blödsinn reinkommt.
ZitatOriginal von kls
Woran scheitert's denn?
Da sich ja sonst keiner traut - aber ich denke Klaus wird das schon nicht falsch verstehen
Also mir fällt es stellenweise schwer die unbegrenzt tiefen Bäume zu lesen (Du hast nicht zufällig bei Wirth oder einem seine Nachfolger gelernt? ) - ich bevorzuge da lieber einen Rücksprung bei Nichterfüllung als einen neuen Block bei Erfüllung (wo möglich)... Auch die scheinbar unbegrenzte Zeilenlänge ist für mich ungewöhnlich. Und die je nach Block-öffnendem Schlüsselwort unterschiedliche Einrückungstiefe.
Aber all das bleibt - wie gesagt - Geschmackssache...
ZitatOriginally posted by LordJaxom
Da sich ja sonst keiner traut :mua- aber ich denke Klaus wird das schon nicht falsch verstehen
Klar.
Zitat
Also mir fällt es stellenweise schwer die unbegrenzt tiefen Bäume zu lesen (Du hast nicht zufällig bei Wirth oder einem seine Nachfolger gelernt? ;)) - ich bevorzuge da lieber einen Rücksprung bei Nichterfüllung als einen neuen Block bei Erfüllung (wo möglich)... Auch die scheinbar unbegrenzte Zeilenlänge ist für mich ungewöhnlich. Und die je nach Block-öffnendem Schlüsselwort unterschiedliche Einrückungstiefe.
Aber all das bleibt - wie gesagt - Geschmackssache...
Da hast du vollkommen recht.
Ich habe zum Beispiel Probleme, mich in Sourcecode zu orientieren, bei dem die schließende Klammer eines Blocks (etwa bei einem 'if') nicht eingerückt ist. Die schließende Klammer ist Teil des (compound) Statements, also was macht sie "vorne", wo sie bloß das eventuelle 'else' "verdeckt"? Gerade über dieses Thema gibt es diverse verschiedene Weltanschauungen, und natürlich ist jeder von seiner eigenen vollkommen überzeugt
Die unterschiedliche Einrücktiefe ergibt sich für mich einfach daraus, daß das Schlüsselwort, welches das Einrücken verursacht, deutlich und "frei" dastehen soll. Und das ist halt nunmal bei
deutlicher als bei
daher rücke ich immer bis zum ersten Zeichen nach dem Schlüsselwort ein. Im Beispiel oben bilden die runde Klammer, die ersten Buchstaben der statements und die schließende geschweifte Klammer eine gerade Linie, an der sich das (zumindest mein) Auge sofort orientieren kann. Im unteren Fall fehlt diese gerade Linie, und es sieht dazu auch noch so aus, als könnte 'statement2' jeden Moment nach unten "rausfallen", weil nichts da ist, das es "stützt".
Aber laßt uns hier keine Grundsatzdiskussion über Sourcecode-Formatierung anfangen. Ich schreibe seit einiger Zeit Software und werde meinen Formatierungsstil (den ich im übrigen - ich weiß nicht, ob das schon klar wurde - für den einzig richtigen halte nicht ändern. In seinem eigenen Code (z.B. in VDR-Plugins) kann jeder nach Herzenslust seinem eigenen Stil frönen.
Klaus
ZitatOriginally posted by Dieter
...
Klaus hat mal angefangen mit doxygen,...
Aber auch nur, weil jemand eine Patch dafür geschickt hat und meinte, das müsse unbedingt rein
Zitat
...aber man findet noch nicht sehr viel davon.
Na ja, an manchen Stellen habe ich seitdem durchaus spezielle Kommentare hinterlassen.
Zitat
Beispiel aus einem USB Treiber (mit doxygen). Leider mag die forensoftware meine Tabs nicht so gerne.
Gutes Stichwort: Tabs in Sourcecode finde ich absolut daneben! Erstmal muß man meistens erraten, welche Tabstops der Autor denn hatte, als er seinen Code formatierte. Und dann geht auch noch jeder Editor anders damit um. Einen mit Tabulatoren verseuchten Block anders einzurücken ist immer wieder ein Quell der Freude. Wenn mir jemand Code schickt ist mein erster Befehl meist
:%!expand
Also: Tabs in Sourcecode == NoNo!
Zitat
Man sieht auch das Bischen unsägliche Notation das ich Verwende.
Wenn du damit die "m_" vor den Namen der Members meinst - tja, den tieferen Sinn dahinter verstehe ich auch nicht.
Zitat
Gestern habe ich versucht die records.c zu verstehen.
Ich vermute mal, du meinst recording.c?!
Zitat
Probleme hatte ich zum Beispiel herauszufinden was in der Variablen FileName drin ist. Ist es /video/Film/2006.03.31.50.50.rec oder nur ein Teil davon. Kommentare wie in meinem Beispiel hätten schon geholfen.
@Klaus: Bitte nicht falsch verstehen, ich möcht absolut nicht meckern.
Wenn Du willst, kann ich bei meinen Experimenten passende Kommentare hinzufügen und dir schicken. Solltest sie aber gut prüfen damit kein Blödsinn reinkommt.
Momentan ist dafür eine sehr schlechte Zeit, denn ich mache jetzt nur noch Dinge, die unbedingt noch für die Version 1.4 gemacht werden müssen. Alles andere nehme ich durch meine virtuellen Scheuklappen derzeit gar nicht wahr
Klaus
Naja , bin zwar kein Programmierer aber das 2. Beispiel finde ich uebersichtlicher.
Wenns nur 3-4 Zeilen sind , ist es ja noch uebersichtlich.
Aber wenn sich die Funktion ueber mehr als eine Seite erstreckt und man
haengt nicht gerade an ner GUI und da kommt nen ...blah..parantheses..blahh
oder man moechte mittendrin noch was einfuegen , sehe ich im 1. Beispiel besser wo.
Agesehen davon das ich mir nen if Statement sparen koennte mal nen
harmloses Beispiel :
if (ItemIndex == 0) {
if(isWebstream) {
if(mgr->currIndex==mgr->currindex) {
Skins.Message(mtError, tr("You can't delete active stream"));
return osContinue;
}
}
if(MP3Setup.AdminMode) {
bool Result = mgr->Del_Record(mgr->currIndex);
if (!Result) return AddSubMenu(new cMenuText(tr("Error:"), tr("ERROR: Could not delete track from playlist !"), fontFix));
return osBack;
}
else {
if(Interface->Confirm(tr("Delete track from playlist ?")) && Interface->Confirm(tr("Are you sure?")) ) {
Skins.Message(mtStatus, tr("Delete track from playlist..."));
bool Result = mgr->Del_Record(mgr->currIndex);
Skins.Message(mtStatus, NULL);
if (Result)
return AddSubMenu(new cMenuText(tr("Delete track from playlist:"), tr("Track deleted from current playlist !"), fontFix));
else
return AddSubMenu(new cMenuText(tr("Error:"), tr("ERROR: Could not delete track from playlist !"), fontFix));
}
}
}
Alles anzeigen
imho uebersichtlicher als so :
// Delete Track from playlist and save as current playlist in ConfigDir;;
if (ItemIndex == 0) {
if(isWebstream) {
if(mgr->currIndex==mgr->currindex) {
Skins.Message(mtError, tr("You can't delete active stream"));
return osContinue;
}
}
if(MP3Setup.AdminMode) {
bool Result = mgr->Del_Record(mgr->currIndex);
if (!Result) return AddSubMenu(new cMenuText(tr("Error:"), tr("ERROR: Could not delete track from playlist !"), fontFix));
return osBack;
}
else {
if(Interface->Confirm(tr("Delete track from playlist ?")) && Interface->Confirm(tr("Are you sure?")) ) {
Skins.Message(mtStatus, tr("Delete track from playlist..."));
bool Result = mgr->Del_Record(mgr->currIndex);
Skins.Message(mtStatus, NULL);
if (Result)
return AddSubMenu(new cMenuText(tr("Delete track from playlist:"), tr("Track deleted from current playlist !"), fontFix));
else
return AddSubMenu(new cMenuText(tr("Error:"), tr("ERROR: Could not delete track from playlist !"), fontFix));
}
}
}
Alles anzeigen
Ok , die vorletzte Klammer habe ich uebertrieben.
Wenn ich die oeffnende Klammer nicht sehe , kann ich oben wenigstens abzaehlen
ZitatAlles anzeigenOriginal von kls
Die unterschiedliche Einrücktiefe ergibt sich für mich einfach daraus, daß das Schlüsselwort, welches das Einrücken verursacht, deutlich und "frei" dastehen soll. Und das ist halt nunmal bei
deutlicher als bei
daher rücke ich immer bis zum ersten Zeichen nach dem Schlüsselwort ein.
Klaro, das macht nicht zuletzt auch emacs so - wobei ich die Klammer zu gerne vorne habe
Hi,
ZitatOriginal von kls
Wenn du damit die "m_" vor den Namen der Members meinst - tja, den tieferen Sinn dahinter verstehe ich auch nicht.
Weil ich schon ein paar Patches für VDR geschrieben habe, möchte ich hier auch was beitragen. Ich habe den Code in der Umgebung des durch den Patch betroffenen Bereichs mehrfach studiert, um herauszufinden, welche Schreibweise für Variablen anzuwenden ist.
Member-Variablen werden anscheinend klein geschrieben während lokale Variablen und Parameter groß geschrieben werden (klein / groß bezieht sich auf das erste Zeichen des Bezeichners). Letzteres trifft aber anscheinend auf kurze Bezeichner (wie i oder p) nicht zu.
Schnell ergeben sich dann Benennungskonflikte, wie im jüngsten Speedup-Patch:
const uchar *p = Data + Offset + PesPayloadOffset + 2;
const uchar *pLimit = Data + Offset + Length - 3;
pLimit ist eigentlich falsch, doch ich wollte damit den Bezug zu p herstellen.
Aber es ist akzeptiert worden
Bye.
kls
Auch wenn ich persöhnlich einen Ganz anderen Stiel beim Coden bevorzuge und seit Jahren selbst nix grösseres mehr auf die Beine gestellt habe finde ich die VDR-Sourcen an sich tip top! Wenn 'meine' proffesionellen Designer...Programmier ähnliches leisten würden, bräuchte ich nicht regelmässig um meine Tantieme fürchten...Einzig die Dokumentation des Gesamtsystems scheint mir etwas dünn.
Wie weiter oben schon mal angesprochen scheint mir zunächst die Pluginschnittstelle revisionsbedürftig. Siehst Du einen Möglichkeit, die so zu überarbeiten, dass nicht jedes Plugin für jede VDR-Version und jedes Patchlevel neu übersetzt werden muss bzw. warum ist das eigentlich notwendig?
Zitat
Woran scheitert's denn?
War nicht persönlich gemeint.
Ich habe mich auch daran gewöhnt, nur kann sowas für einen Anfänger eine ganz andere Hürde darstellen.
Die Einrückung - eine kreative Mischung aus Whitesmith mit ein wenig K&R - passt nicht zu meiner persönlichen Vorliebe (K&R Java-Style), wodurch ich gelegentlich die schließende Klammer falsch zuordne. Gewöhnungssache.
Tja, mal sehen, was mir noch an Stolpersteinen einfällt...
Aus plugin.c:
Kreativ, kompakt, aber ein, zwei Momente braucht man, um es zu verstehen. Ich würde drei Zeilen mehr investieren:
Zugegeben, kein normaler Plugin-Programmierer wird diesen Code lesen.
Eine Falle, in die ich mal getappt bin: cListObject, cListBase und cList<> aus den tools. Weit verbreitet in VDR, aber weder in tools.h noch in tools.c wesentliche Kommentare. Naja, scheint ja auch einfach. Da gibt es zb. hilfreiche Funktionen in cListObject, namens Append(), Insert() und Unlink(), die scheinbar gut geeignet sind, um Einträge in die Liste aufzunehmen oder zu entfernen. Funktioniert sogar - begrenzt.
Tatsächlich sind sie nur zu internem Gebrauch bestimmt, und aktualisieren cList<> nicht. Die Folge: Count() stimmt nicht, manchmal verschwinden neue Einträge, oder werden bei Unlink() nicht richtig aus der Liste entfernt. Auffallen tut es erst, wenn die libc meckert, der Speicherblock könne nicht freigegeben werden - zum zweiten Mal.
Noch ein Beispiel, mit dem ich zu tun hatte: cRingBufferLinear. Immerhin etwas dokumentiert. Um aber genau zu verstehen, wie sich der Ringbuffer in verschiedenen Situationen verhält, muss man den Code lesen. Der ist natürlich wieder unkommentiert, und kompliziert. Es hat einige Zeit gedauert, bis ich verstanden hatte, wie der Buffer intern funktioniert. (Wer wäre zum Beispiel darauf gekommen, dass der maximale Füllstand nicht Size ist, sondern Size-Margin?)
Zum Glück funktioniert er, darin einen Fehler zu suchen wäre ein Alptraum.
Wie gesagt, das ist nicht als Kritik gemeint. Es heißt auch nicht, dass Klaus' Code schlecht ist. Es sind mehr Stolpersteine, die für Anfänger schwer zu meistern sind.
Vielleicht sollten wir uns zusammen tun, und im Wiki ein Dokumentationsprojekt starten, um unser Wissen über die Interfaces zusammen zu tragen. Nach Möglichkeit gleich in Englisch. Es muss ja nicht gleich die Dokumentation zu cDevice sein, ein Anfänger braucht eher schon Infos zu Listen, OSDs und Menüs. (Oder wer kennt spontan den Unterschied zwischen cMenuEditStrItem und cMenuEditStraItem? )
ZitatSiehst Du einen Möglichkeit, die so zu überarbeiten, dass nicht jedes Plugin für jede VDR-Version und jedes Patchlevel neu übersetzt werden muss bzw. warum ist das eigentlich notwendig?
VDR hat keine klare Abgrenzung zwischen Plugin-Schnittstelle und allgemeinen VDR-Internas. Jedes Plugin hat vollen Zugriff auf jede Funktion und Variable von VDR, in den Grenzen, die auch zwischen den VDR-eigenen *.c-Dateien gelten. Alles, was in Header-Dateien vorkommt, steht Plugins zur Verfügung. Dadurch sind VDR-Plugins natürlich sehr mächtig. Andererseits kann schon die kleinste Änderung an einer Datenstruktur in einer Header-Datei auch das neuübersetzen der Plugins erfordern. Ein neuer Setup-Parameter zum Beispiel genügt, um die Variablen in cSetup neu anzuordnen. Ist das Plugin nicht daran angepasst, greift es im Speicher daneben.
Gruß,
Udo
Zitat
Ok , die vorletzte Klammer habe ich uebertrieben.
Wenn ich die oeffnende Klammer nicht sehe , kann ich oben wenigstens abzaehlen
Also "richtig" formatiert müsste das so aussehen:
// Delete Track from playlist and save as current playlist in ConfigDir:
if (ItemIndex == 0) {
if (isWebstream) {
if (mgr->currIndex == mgr->currindex) {
Skins.Message(mtError, tr("You can't delete an active stream"));
return osContinue;
}
}
if (MP3Setup.AdminMode) {
bool Result = mgr->Del_Record(mgr->currIndex);
if (!Result)
return AddSubMenu(new cMenuText(tr("Error:"), tr("ERROR: Could not delete track from playlist!"), fontFix));
return osBack;
}
else {
if (Interface->Confirm(tr("Delete track from playlist?")) && Interface->Confirm(tr("Are you sure?"))) {
Skins.Message(mtStatus, tr("Delete track from playlist..."));
bool Result = mgr->Del_Record(mgr->currIndex);
Skins.Message(mtStatus, NULL);
if (Result)
return AddSubMenu(new cMenuText(tr("Delete track from playlist:"), tr("Track deleted from current playlist!"), fontFix));
else
return AddSubMenu(new cMenuText(tr("Error:"), tr("ERROR: Could not delete track from playlist!"), fontFix));
}
}
}
Alles anzeigen
BTW: das ist auch so eine "Untugend", zwischen 'if' und der öffnenden Klammer kein Blank zu setzen. Ebenso vor und nach Operatoren wie '=='.
Aber ich wollte mich eigentlich nicht in eine Formatier-Diskussion reinziehen lassen...
Klaus
ZitatAlles anzeigen
VDR hat keine klare Abgrenzung zwischen Plugin-Schnittstelle und allgemeinen VDR-Internas. Jedes Plugin hat vollen Zugriff auf jede Funktion und Variable von VDR, in den Grenzen, die auch zwischen den VDR-eigenen *.c-Dateien gelten. Alles, was in Header-Dateien vorkommt, steht Plugins zur Verfügung. Dadurch sind VDR-Plugins natürlich sehr mächtig. Andererseits kann schon die kleinste Änderung an einer Datenstruktur in einer Header-Datei auch das neuübersetzen der Plugins erfordern. Ein neuer Setup-Parameter zum Beispiel genügt, um die Variablen in cSetup neu anzuordnen. Ist das Plugin nicht daran angepasst, greift es im Speicher daneben.
Gruß,
Udo
Genau. Ich verstehe auch ehrlich gesagt nicht, was daran so schlimm ist, wenn man alles zusammen übersetzt. Sicher, während einer Entwicklungsphase kommt das wohl häufig vor, und leider hat diese Phase nunmal wesentlich länger gedauert als ich mir das gewünscht hätte. Aber ich fürchte, es wäre ein unverhältnismäßig hoher Aufwand, alles so "abzuschotten", daß verschiedene Program- und Pluginversionen zusammen funktionieren. Da ist es doch wesentlich einfacher, einfach alles zusammen zu übersetzen.
Klaus
Sie haben noch kein Benutzerkonto auf unserer Seite? Registrieren Sie sich kostenlos und nehmen Sie an unserer Community teil!