Sto sviluppando un’applicazione che è una estensione di un’applicativo , il tutto in managed c++. Per caricare i miei assemblies di varie funzioni specializzate, avevo usato la strada della GAC e quella della modifica application.exe.config.
Soluzione1 non bella, perchè queste dll non erano poi cosi’ tanto generalpurpose e poi mi potevano interferire con altre installazioni.
Soluzione 2 non bella perchè andavo a modificare dei file proprietari dell’applicativo, ad un suo re-install venivano perse le mie modifiche. Ho trovato in rete la soluzione 3, la riassumo J
Nella fase di caricamento : prendo il dominio corrente dell’applicazione e metto un gestore dell’evento di errore nel caricamento degli assemblies:
AppDomain^ currentDomain = AppDomain::CurrentDomain;
currentDomain->AssemblyResolve +=
gcnew System::ResolveEventHandler( &miaclasse::MyResolveEventHandler);
e poi lo gestisco, andando a prendere l’assembly proprio nella mia dir:
System::Reflection::Assembly ^ miaclasse::MyResolveEventHandler(Object^ sender, ResolveEventArgs^ args)
{ //------ qui prendo il mio percorso di installazione dei miei componenti
//e lo metto in String^ path -------
//nome dell’assembly da caricare
String^ strTempAssmbPath=args->Name->Substring(0,args->Name->IndexOf(","))+".dll";
//percorso completo
String^ fullPath = System::IO::Path::Combine(path, strTempAssmbPath);
//carico il tutto e lo ritorno !
Assembly^ MyAssembly = Assembly::LoadFrom(fullPath);
return MyAssembly;
}
Funziona!
Nella gestione dei file e delle directory, in System.IO, si utilizzano principalmente: File, FileInfo, Directory, Directory Info e la DriveInfo. La DriveInfo è bella perchè da un sacco di informazioni sul disco:
DriveInfo d = new DriveInfo("d:\\");
String info = "Drive: " + d.Name +
"Drive Format " + d.DriveFormat +
"\nDrive Type " + d.DriveType +
"\nVolume " + d.VolumeLabel +
"\nTotal Size " + d.TotalSize.ToString() +
"\nAvailable FreeSpace " + d.AvailableFreeSpace.ToString() +
"\nTotal Free Space " + d.TotalFreeSpace.ToString();
Console.WriteLine(info);
dove DriveType indica CDRom, Fixed, Network, Removable,Ram,.. e DriveFormat dice NTFS or FAT32.
Interessante anche il FileSystemWatcher per le notifiche, su cui c’e’ anche un post di Mirko Gatti, http://blogs.ugidotnet.org/mitch/archive/2006/09/22/48513.aspx . Confesso che con il framework 2.0 nei miei test questo non ho verificato l’errore in creazione, ma in un caso di copia di file.
Per giocare un po’ mi sono fatta una funzioncina di utilità che copia tutta una directory e assegna a tutti i file e directory una certa data di creazione/ultima modifica. Questa me la metto in una console application e do’ il risultato il pasto al setup e ho tutte le cose belle ordinate. Magari c’e’ un’opzione da qualche parte che lo fa da solo, pero’ con questa ci posso poi mettere dei filtri e saltare alcuni tipi di file...
private static void CopyAll(string from, string to, DateTime date)
{
//dati delle directory di partenza
DirectoryInfo dFrom = new DirectoryInfo(from);
//dati delle directory di destinazione
DirectoryInfo dTo = new DirectoryInfo(to);
if (!dTo.Exists)
{
//ricreo la directory
System.IO.Directory.CreateDirectory(to);
dTo.CreationTime = date; //metto le date di creazione, modifica
dTo.LastWriteTime = date;
}
//leggo tutti i file
FileInfo[] files = dFrom.GetFiles();
foreach (FileInfo f in files)
{
//copio il file
string newFile = System.IO.Path.Combine(to, f.Name);
System.IO.File.Copy(f.FullName, newFile );
FileInfo fNew = new FileInfo(newFile);
//determino se è readonly
bool bReadOnly = (fNew.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly;
//se è readonly .. tolgo l'attributo
if (bReadOnly) fNew.Attributes = fNew.Attributes ^ FileAttributes.ReadOnly;
fNew.CreationTime = f.CreationTime; //assegno data di creazione originale
fNew.LastWriteTime = date; //assegno date di ultima modifiche/accesso
fNew.LastAccessTime = date;
//se occorre rimetto a posto il readonly
if (bReadOnly) fNew.Attributes = fNew.Attributes ^ FileAttributes.ReadOnly;
}
//vado in ricorsione per le directory
DirectoryInfo[] dir = dFrom.GetDirectories();
foreach (DirectoryInfo d in dir)
CopyAll(d.FullName, System.IO.Path.Combine(to, d.Name), date);
}
Per completare il lavoro serve una funzione di DeleteAll della directory di destinazione, prima di iniziare la copia.
Se ci sono file ReadOnly non ci si puo’ limitare ad usare dTo.Delete(true), viene un’eccezione.. occorre andare a togliere l’attributo.
Gioco un po’ quello che c’e’ in System.Xml.Serialization, anche perchè in TV non c’e’ (al solito) nulla di umano da vedere... ed il resto (saggio) della famiglia dorme.
La classe per serializzare è XmlSerializer, che si avvale di una bella dose di attributi sulle proprietà delle classi , es.
XmlRootAttribute : elemento radice dell’oggetto
XmlElementAttribute = elemento xml classico
XmlAttributeAttribute= elemento di tipo attributo
E i suoi amici.. XmlTextAttribute, XmlTypeAttribute, XmlEnumAttribute.., XmlArray
(ci sono anche per soap e si chiamano SoapAttributeAttribute, SoapEnumAttribute....).
[XmlRootAttribute("IlMioOggetto", Namespace = "http://www.bruna.com", IsNullable = false)]
public class mioOggetto2
{
[XmlAttributeAttribute]
public string id;
[XmlElementAttribute( IsNullable = false, DataType="string" )]
public string nome; //se sono private non si serializzano
[XmlElementAttribute] //è cmq il default
public string cognome;
[XmlElementAttribute(ElementName="Eta", DataType="int")]
public int eta;
[XmlArray]
public List<string> nipotini;
public mioOggetto2()
{
// questo è necessario perchè altrimenti XmlSerializer si rifiuto di inizializarsi
}
public mioOggetto2(string n, string c, int e)
{
id = System.Guid.NewGuid().ToString();
nome = n;
nipotini = new List<string>();
eta = e;
}
public void addNipote(string nome) { nipotini.Add(nome); }
}
E si utilizza in un bel xmlSerializer
static void Main(string[] args)
{
mioOggetto2 obj1 = new mioOggetto2("Donald", "Duck");
obj1.addNipote("Qui"); obj1.addNipote("Quo"); obj1.addNipote("Qua");
XmlSerializer ser = new XmlSerializer(typeof(mioOggetto2));
TextWriter txtW = new StreamWriter("c:\\temp\\serialize1.xml");
ser.Serialize(txtW, obj1);
txtW.Close();
}
Ed il risultato è:
<?xml version="1.0" encoding="utf-8" ?>
- <IlMioOggetto xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" id="43e35b0b-e94f-47f7-a2eb-9e4db3c682e1" xmlns="http://www.bruna.com">
<nome>Donald</nome>
<Eta>0</Eta>
- <nipotini>
<string>Qui</string>
<string>Quo</string>
<string>Qua</string>
</nipotini>
</IlMioOggetto>
E per rileggere? Nel main:
XmlSerializer ser2 = new XmlSerializer(typeof(mioOggetto2));
TextReader txtR = new StreamReader("c:\\temp\\serialize1.xml");
mioOggetto2 m = (mioOggetto2) ser2.Deserialize(txtR);
E avro’ tutto a posto..
Un esempio molto bello, c’e’ nell’MSDN in c++ , nella classe XmlSerializer.
|
Rimettiamoci a studiare dopo un po’ di vacanza e un po’ di eccessivo lavoro (tanto per bilanciare).
Serialize or deserialize an object or an object graph using runtime serialization techniques (System.Runtime.Serialization namespace) |
Le interfacce di serializzazione permettono di controllare tutto il processo e per implementarle è necessario implementare l’interfaccia ISerializable, che espone il metodo GetObjectData in cui viene effettuato il lavoro.
Ci provo:
[Serializable] //senza questo non va nulla
public class mioOggetto : ISerializable, IEquatable<mioOggetto >
{ private string nome;
private string cognome;
public mioOggetto(string n, string c)
{
nome = n;
cognome = c;
}
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("AttributoNome", nome);
info.AddValue("AttributoCognome", cognome);
}
#region IEquatable<mioOggetto> Members
public bool Equals(mioOggetto obj2)
{
if (obj2.nome == nome && obj2.cognome == cognome ) return true;
else return false;
}
#endregion
}
Per la serializzazione ho bisogno di un oggetto che implementa l’interfaccia IFormatter, per esempio System.Runtime.Serialization.Formatters.Soap.SoapFormatter , e anche di uno stream cosi’ me lo salvo su disco e controllo il risultato.
static void Main(string[] args)
{
mioOggetto obj1 = new mioOggetto("Donald","Duck");
mioOggetto obj2 = new mioOggetto("Mickey","Mouse");
IFormatter fmt = new System.Runtime.Serialization.Formatters.Soap.SoapFormatter();
System.IO.Stream stream = new System.IO.FileStream("c:\\temp\\testSerialize.xml", System.IO.FileMode.OpenOrCreate );
fmt.Serialize( stream, obj1);
fmt.Serialize( stream, obj2);
stream.Close();
}
Il risultato è un file file xml..cosi’:
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" …….. /">
<SOAP-ENV:Body>
<a1:mioOggetto id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/Test3/Test3%2C%20Version%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<AttributoNome id="ref-3">Donald</AttributoNome>
<AttributoCognome id="ref-4">Duck</AttributoCognome>
</a1:mioOggetto>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
<SOAP-ENV:……..
Per deserializzare il gioco è praticamente identico.. ho bisogno di un altro SoapFormatter e gli dico di deserializzare, aggiungo al mio main le seguenti righe...
IFormatter fmt2 = new System.Runtime.Serialization.Formatters.Soap.SoapFormatter();
System.IO.Stream stream2 = new System.IO.FileStream("c:\\temp\\testSerialize.xml", System.IO.FileMode.Open );
mioOggetto ret1 = (mioOggetto) fmt2.Deserialize(stream2);
mioOggetto ret2 = (mioOggetto) fmt2.Deserialize(stream2);
if (ret1.Equals( obj1) ) Console.Write("Bene.");
if (ret2.Equals( obj2) ) Console.Write("..bene!");
Ma attenzione, il gioco non è finito, se si prova il tutto cosi’ la funzione Deserialize si imbufalisce perchè dice che non ha il costruttore adeguato per la classe. La soluzione? Ovvia, quando la si è scoperta: aggiungere il costruttore che prende gli stessi parametri della GetObjectData e fa il lavoro inverso. Cosi’:
public mioOggetto( SerializationInfo info, StreamingContext cnt)
{
nome = info.GetString ("AttributoNome") ;
cognome = info.GetString ("AttributoCognome");
}
Ci sono altre interfacce per controllare la serializzazione:
IDeserializationCallback: indica la classe deve essere richiamata quando la deserializzazione dell’intero insieme degli oggetti è completata.
IFormatterConverter: fornisce la connessione fra SerializationInfo e la classe formatter, in genere utilizzata per fare il parsing dei dati all’interno delle Info.
IObjectReference: indica che la classe è il riferimento ad un altro oggetto
ISerializationSurrogate: utilizzata per permettere ad un oggetto di effettuare la serializzazione di altri, magari logicamente dipendenti.
Implement .NET Framework interfaces to cause components to comply with standard contracts
E’ buona norma uniformare il comportamento delle proprie classi a quelli degli oggetti standard del framework, in tal modo l’uso nell’applicazione non è un “caso speciale”, ma è assolutamente coerente con il resto. Ci sono percio’ delle interfaccie pensate apposta:
IComparable: ha un unico metodo CompareTo per effettuare controlli ed ordinamenti , l’ho usata in [70-553] – 1. Section 1 – 1. Dev application – 1 – Manage data (4)
IDisposable: definisce il metodo Dispose per gestire una eventuale disallocazione specifica e dettagliata delle risorse.
IConvertible: permette di definire diversi metodi di conversione della classe in interi, stringhe, date..etc..
ICloneable: per avere un metodo Clone che copia l’oggetto in ogni sua parte
INullableValue: per avere il metodo IsNull, per indicare che una istanza (magari di tipo valueType) è nulla. Es. ObjectId nell’Autodesk DOTNETARX ha il metodo IsNull per indicare identificativo nullo, è un metodo che semplicemente controlla che l’identificativo interno dell’id sia ancora al valore 0, iniziale, generato dal costruttore di default.
IFormattable: ha il metodo ToString().. quindi alcuni giorni fa avrei dovuto usarlo...
IEquatable: ha il metodo Equals per contollare che due oggetti siano da considerare uguali.
Nei miei testi, per controllare che due punti siano uguali, non è possibile controllare che x,y,z siano identici, perchè puo’ succedere che qualche cifra decimale da un certo punto poi sia differente, soprattuto in caso di operazioni matematiche.
Definisco allora che la classe implementi l’interfaccia IEquatable per effettuare l’operazione di compare: due punti sono identici se la loro distanz a è sotto una certa soglia.
public struct PuntoStruct: IComparable < PuntoStruct >, IEquatable<PuntoStruct>, IFormattable
{
private double _x, _y, _z;
public PuntoStruct(double x1, double y1, double z1)
……
public bool Equals(PuntoStruct other)
{
double d = Math.Sqrt(Math.Pow(_x - other._x, 2.0) +
Math.Pow(_y - other._y, 2.0) +
Math.Pow(_z - other._z, 2.0));
return (d < 0.0001);
}
}
Che si usa :
PuntoStruct p0 = new PuntoStruct(0, 0, 0);
PuntoStruct p1 = new PuntoStruct(0.000001, 0.000001, 0.000001);
PuntoStruct p2 = new PuntoStruct(2, 4, 3);
if (p1.Equals(p0))
Console.WriteLine(" ( {0} ) == ( {1} ) ", p0.ToString(), p1.ToString());
if (p2.Equals(p0))
Console.WriteLine(" ( {0} ) == ( {1} ) ", p0.ToString(), p2.ToString());
E’ chiaro che p1 viene considerato identico a p0, perchè la loro distanza è sotto la soglia di 0.0001,
mentre p2 è diverso da p0!
|
Improve type safety and application performance by using generic collections (System.Collections.Generic namespace) |
Si tratta delle nuove collezioni, basate sui tipi generici e che quando vengono instanziate “acquisiscono” i tipi. Sono quindi implicitamente piu’ sicure di quelle tradizionali perchè una collezione di un certo tipo potrà contenere solo elementi di quel tipo, non elementi differenti. Nelle collezioni tradizionali questo non era vero, erano omogenee solo per il tipo di base (Object), ma in pratica potevano contenere pere/mele/rinoceronti in modo indistinto. Le collezioni sono anche piu’ performanti perchè non fanno boxing/unboxing con un bel risparmio di risorse.
Le interfacce sono le stesse delle altre collezioni: ICollection, IComparer, IDictionary, IEnumerable, IEnumerator, IEqualityComparer, IList, ma sono generiche es. IList<T>.
System.Collection.Generics System.Collection
Dictionary Hashtable
List Arraylist
Queue Queue
Stack Stack
SortedList SortedList
C’e’ anche una LinkedList , lista bilincata, i cui elementi sono LinkedListNode.
LinkedList<PuntoStruct> mieiPunti = new LinkedList<PuntoStruct>();
mieiPunti.AddFirst(new PuntoStruct(1, 1, 1));
mieiPunti.AddLast (new PuntoStruct(3,3, 3));
//inserisco nella seconda posizione un nuovo punto
LinkedListNode<PuntoStruct> pRef = mieiPunti.First;
mieiPunti.AddAfter(pRef, new PuntoStruct(2, 2, 2));
foreach (PuntoStruct p in mieiPunti)
Console.WriteLine(p.ToString());
Manage a group of associated data using collections (System.Collections namespace)
Classi: Le classi non fanno che implementare le interfacce.:
ArrayList, implementa: IList, ICollection, IEnumerable, ICloneable
Hashtable: implementa IDictionary, ICollection, IEnumerable, ICloneable .. e qualche altra interfaccia non di liste per la serializzazione e deserializzazione;
CollectionBase: è una classe astratta per collezioni fortemente tipizzate. Implementa IList, ICollection, IEnumerable.
Per usarla si deve creare una propria classe derivando da quella..
ReadOnlyCollectionBase: è una classe astratta per una collezione che, dopo la fase di creazione, diventa read-only
DictionaryBase: è la classe astratta per un dizionario: insieme di coppie chiave-valore.
DictionaryEntry: è la classe per definire la coppia chiave – valore.
Queue: è una lista tradizionale (FIFO), implementa ICollection, IEnumerable, Icloneable.
SortedList: è una sorta di dizionario ordinato per chiave, accessibile sia per chiave che per indice. Implementa: ICollection, IEnumerable, ICloneable
BitArray: gestisce in modo compatto un array di bit. Implementa ICollection, IEnumerable, ICloneable.
Stack: è la classica lista LIFO. Implementa: ICollection, IEnumerable, ICloneable
Comparer: serve per paragonare due oggetti, il costruttore puo’ prende la cultura come parametro. C’e’ anche una bella classe CaseInsensitiveComparer bello per paragonare le stringhe
Manage a group of associated data using collections (System.Collections namespace)
Interfacce:
ICollection: è l’interfaccia per quasi tutte le liste: struttura l’interfaccia per inserire un elemento, per inserire un elemento in una posizione, per avere il conteggio e un enumerator che funziona come iteratore, che implementa l’intefaccia IEnumerator.
IComparer: è un’interfaccia che espone solo in metodo Compare per personalizzare il confronti fra gli oggetti della collezione.
IDictionary: è l’interfaccia per una collezione di coppie “chiave – valore”, prevede funzioni come Add, Contains, Clear e la scansione con un IDictionaryEnumerator.
IEnumerable è una interfaccia ha solo una funzione che ritorna un IEnumerator. Il bello di una classe che implementa questo tipo di interfaccia è che si puo’ utilizzare l’enumerator in un foreach rendendo il tutto molto molto semplice nell’uso.
Se si usa un IEnumerator è necessarissimo utilizzare yield per restituire i valori.
public class Mesi : System.Collections.IEnumerable
{
string[] _mesi = {"Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dec"};
public System.Collections.IEnumerator GetEnumerator()
{
for (int i = 0; i < _mesi.Length; i++)
{
yield return _mesi[i];
}
}
}
E si usa:
Mesi mieimesi = new Mesi();
foreach (string s in mieimesi)
Console.Write(" {0} ", s);
IEqualityComparer: è una interfaccia per soli metodi: Equals e GetHashCode.
IHashCodeProvider: è un interfaccia per definire una propria funzione Hash
IList: E’ l’interfaccia per una lista di oggetti: Add, Insert, getIndexOf,Remove,
(ICloneable è l’interfaccia per ottenere una copia dell’oggetto)
Manage data in a .NET Framework application using .NET Framework 2.0 system types (System namespace)
Eccezioni: System.Exception è la classe base di tutte le eccezioni ed interrompono il flusso normale di lavoro dell’applicazione per un errore. Un’eccezione viene gestita dall’applicazione o dal gestore degli errori predefiniti.
Riassumento le eccezioni si intercettano con Try- Catch, dove il catch puo’ essere catch(1), catch(2)... per distinguere a priori su diversi tipi di eccezioni e prenderle in modo gerarchizzato...
Dopo il blocco Try-Catch il Finally viene sempre eseguito, in genere utilizzato per liberare risorse.
C’e’ un ApplicationException che indica un errore irreversibile, ma la guida consiglia di derivare le proprie eccezioni da System.Exception.
Per generare un’eccezione si usa: throw new myException(..parametri);
http://msdn2.microsoft.com/it-it/library/system.exception(VS.80).aspx
Boxing/Unboxing. E’ la tecnica con cui si passano da ValueType a ReferenceType.
object o = i; BOX: dove i è il classico intero o un altro valueType
int i = (int)o; UNBOX: per ottenere il valore indietro.
Ma per sapere cosa c’e’ dentro ad un oggetto.. if (o is int)....
http://www.csharpfriends.com/Articles/getArticle.aspx?articleID=38
TypeForwardedToAttribute: specifica un tipo che si trova in un altro assembly e serve per riorganizzare la posizione delle classi negli assembly senza dover modificare i chiamanti e quindi l’intera applicazione L’idea è semplice
stato iniziale:
· In un assembly A c’e’ una classe X con i suoi metodi
· Altri assembly dell’applicazione utilizzano la classe.
Dopo la ristrutturazione:
· In un assembly B c’e’ la classe X con i suoi metodi, stesso namespace
· Dall’assembly A è stata CANCELLATA la classe X
· Nell’assembly A è stata inserito il riferimento all’assembly B
· Nell’assembly A è stata messa una direttiva del tipo:
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(mionamespace.X)]
· Nell’applicazione vengono messi i due nuovi assembly A e B
· L’applicazione non viene ricompilata, continua a pensare di guardare la libreria A, quest'ultima fa poi la ridirezione a B.
J A me ricorda il concetto della dichiarazione extern, anche se quella era solo per la compilazione, non per il runtime.
http://msdn2.microsoft.com/en-us/library/system.runtime.compilerservices.typeforwardedtoattribute.aspx
Manage data in a .NET Framework application using .NET Framework 2.0 system types (System namespace)
Generics: fanno riferimento a classi e metodi che lavorano allo stesso modo su differenti tipi di oggetti. Quando vengono istanziati, necessitano sempre di un tipo di dati, cosi’ da dargli un “preciso significato” e rappresentano l’implementazione in C# dei template del C++.
Uno dei vantaggi dei generics è quello di far risparmiare sul boxing/unboxing: la collezione generica “List” è l’equivalente di “ArrayList”, se ci mettiamo degli interi o dei double o un altro valueTypes non c’e’ boxing per inserire i dati e unboxing per leggere, cosa decisamente apprezzabile in termini di performance.
Gli esempi in rete implementano tutti gli stack.
Allora faccio una lista :
public class myList1< T > {
….
public void Add(T newValue) {…….}
}
E la cosa bella è che in una lista di interi dichiarata con non ci posso mettere dei double e fare confusione. :-)
Voglio pero’ fare in modo che la lista sia sempre ordinata, indipendentemente dal tipo di contenuto. Faccio una lista 2 e gli dico che il tipo T, implemente l’interfaccia IComparable, cosi’ che poi posso fare una funzione di Add sfrutti il metodo CompareTo. (I tipi di base sono tutti derivati da IComparable!).
public class myList2<T> where T : System.IComparable<T>
{
const int minSize = 10;
private T[] myData = new T[minSize];
private int size=0;
public void Add(T newValue)
{ //tralascio tutti gli oggetti piu' piccoli
int destPos = 0;
for (; destPos < size && myData[destPos].CompareTo( newValue)< 0; destPos++) ;
//sposto tutti quelli piu' grandi
for (int moveThis = size-1; moveThis >= destPos; moveThis--)
myData[moveThis + 1] = myData[moveThis];
//inserisco nel posto libero il mio
myData[destPos] = newValue;
//aggiorno la dimensione ed eventualmente allargo l'array
size++;
if (size == myData.Length) enlargeArray();
}
public void Display(string msg)
{
Console.WriteLine(msg);
for (int i = 0; i < size; i++)
Console.WriteLine(myData[i].ToString() + " ");
}
public void enlargeArray() {...}
}
Deve andare con i tipi di base, con i double, con qualsiasi cosa che implementi l’IComparer, anche un ValueType. Modifico la classe PuntoStruct di alcuni giorni fa:
public struct PuntoStruct: IComparable < PuntoStruct >
{
private double _x, _y, _z;
public PuntoStruct(double x1, double y1, double z1) {…}
public int CompareTo( PuntoStruct other)
{
//faccio un ordinamento solo per X, tanto per provare
return (_x.CompareTo(other._x));
}
public override string ToString() {
return (string.Format("x = {0}, y ={1}, z = {2}", _x, _y, _z)); }
};
Se la uso posso istanziare tranquillamente sia una lista di interi:
myList2<int> lista2 = new myList2<int>();
che una lista dei miei punti
myList2<PuntoStruct > listaPunti = new myList2<PuntoStruct>();