Besides being collections of fields, records in Delphi and structures in C# can also have properties, methods, constructors, and operator overloads. While classes in Delphi and C# are always allocated on the heap, records, even those with methods and overloaded operators, are stored on the stack. Because records are allocated on the stack, your code is more efficient since stack allocation is faster than heap allocation. Since records in .NET aren't allocated on the managed heap, they are also not garbage collected, which again results in better overall performance.
Besides the allocation difference, there are several more differences between classes and records:
Classes support inheritance; records don't.
Classes support polymorphism; records don't.
Classes can have parameterless (default) constructors; records cannot. (In C#, records have an implicit parameterless constructor that is called to initialize record fields when you use the new operator. In Delphi, fields get initialized automatically, so there's no need to call the default constructor Create, and actually, you cannot call it.)
In Delphi for Win32 only records support operator overloading; classes don't. (In C# and Delphi for .NET, both classes and records support operator overloading.)
Records don't have destructors; classes do.
Listings 30-7 and 30-8 show how records with fields, properties, methods, and constructors work in Delphi (both flavors) and C#.
Listing 30-7: Records in Delphi for Win32 and Delphi for .NET
program Project1; {$APPTYPE CONSOLE} uses SysUtils; type TMyRecord = record private FX: Integer; FY: Integer; procedure SetX(Value: Integer); procedure SetY(Value: Integer); public constructor Create(newX, newY: Integer); procedure Display(const Prefix: string); property X: Integer read FX write SetX; property Y: Integer read FY write SetY; end; constructor TMyRecord.Create(newX, newY: Integer); begin // assign to properties to call SetX and SetY // to take care of negative values X := newX; Y := newY; end; procedure TMyRecord.SetX(Value: Integer); begin if Value >= 0 then FX := Value else FX := 0; end; procedure TMyRecord.SetY(Value: Integer); begin if Value >= 0 then FY := Value else FY := 0; end; // Prefix is used only to get more meaningful output procedure TMyRecord.Display(const Prefix: string); begin WriteLn(Prefix, 'X = ', Self.X); // or just X WriteLn(Prefix, 'Y = ', Self.Y); // or just Y WriteLn; end; var a: TMyRecord; b: TMyRecord; c: TMyRecord; begin { fields are initialized to 0, displays X = 0, Y = 0 } a.Display('a.'); { using the custom constructor to initialize fields } b := TMyRecord.Create(100, 200); b.Display('b.'); { copies the entire record, not a reference (pointer) } c := b; c.X := 1; c.Display('c.'); // c.X is 1 b.Display('b.'); // b.X remains 200 ReadLn; end.
Listing 30-8: Structures in C#
using System; namespace Wordware.Records { struct MyRecord { private int fx; private int fy; // x property public int X { get { return fx; } set { fx = value >= 0 ? value : 0; } } // y property public int Y { get { return fy; } set { fy = value >= 0 ? value : 0; } } /* custom constructor */ public MyRecord(int newX, int newY) { fx = newX; fy = newY; } /* another constructor that accepts strings */ public MyRecord(string newX, string newY) { fx = System.Int32.Parse(newX); fy = System.Int32.Parse(newY); } // there's no need for const when passing strings in .NET public void Display(string prefix) { Console.WriteLine("{0}X = {1}", prefix, fx); Console.WriteLine("{0}Y = {1}", prefix, fy); Console.WriteLine(); } } class RecordUser { [STAThread] static void Main(string[] args) { /* if you don't use the new operator, the fields are unassigned, and Console.WriteLine doesn't compile */ // MyRecord a; // Console.WriteLine(a.X); /* initialized to default values */ MyRecord b = new MyRecord(); b.Display("b."); /* call the custom (int, int) constructor */ MyRecord c = new MyRecord(1, 2); c.Display("c."); /* call the custom (string, string) constructor, System.Int32.Parse understands spaces */ MyRecord d = new MyRecord(" 2 ", "10"); d.Display("d."); /* struct assignment */ MyRecord e = new MyRecord(1000, 1001); MyRecord f = e; // only if "e" is completely initialized f.Display("f."); Console.ReadLine(); } } }