views:

1637

answers:

18

When I'm reading SO users' comments, I read many small useful tips, so I'm opening this thread to collect such tips.

For example, in this thread Oliver Giesen says:

First of all: Don't pass a non-nil Owner to Create if you're going to free the objects yourself anyway. This just adds lots of unnecessary overhead.

+7  A: 

First thing to do after installing Delphi:

turn on auto-save (Tools/options/Environment Options/Autosave options)

You first have to lose hours of work before you realise that at some point in the 90's an imbecile at Borland thought it was a good idea to leave this off by default.

While you're in the options dialog, change the file backup limit from 10 to 90 (it's the maximum). I find the default 10 not to be enough, unless I'm submitting buggy code into the versioning system all the time. Disk space is cheap.

Wouter van Nifterick
and smart indent
eKek0
+8  A: 

Don't use with. Ever. It may make your code appear a bit cleaner and easier to read, but that comes at the expense of debuggability. (Is that a word?) There's no way to tell the compiler what's supposed to use the with and what isn't, so you can end up with a mess like this:

procedure TMyForm.button1Click(Sender: TObject);
begin
  with Edit1 do
    Caption := 'Hello World!';
end;

Looks good, right? Except that TEdit doesn't have a Caption property; it has a Text property. But this will still compile, because your Form has a Caption property, and you end up doing something completely different than you expected.

With would be a whole lot better if its syntax worked the way VB does it, but in its current form it's too dangerous to be actually useful. Consider it syntactic NutraSweet: tastes a whole lot like syntactic sugar, but it leaves a weird aftertaste and it's actually bad for your health in the long term.

Mason Wheeler
Was writing a comment, but I turned it into a separate comment.. forum-stylee. Your suggestion is good as a rule of thumb, but I find it too extreme to never-ever use "with".
Wouter van Nifterick
+1, in 9 years I have never, ever found a single case where with was both required and made things better in the long run.
Mihai Limbășan
Can't agree with that. `With` is one of the sweetest little Delphi features and I miss it ever so much in Java. @Mihai - ever needed to draw on a canvas? with image1.picture.bitmap.canvas do begin brush.color:=clBlack; fillrect(cliprect); etc... end; Now that's GREAT
Peter Perháč
var canvas: TCanvas; \ begin \ canvas := image1.picture.bitmap.canvas; \ canvas.brush.color := clBlack; \ etc... (Much easier to read and understand what's going on, and doesn't confuse the debugger.)
Mason Wheeler
+2  A: 

DPROJ files have a tendency of getting corrupted and out of sync with the DPR. One way to easily locate non-existent file references within the DPROJ is to use the Class Browser from GExperts. It'll give you an error dialog for every non-existent file reference it finds. Copy-paste these into Notepad and when it's done you have a handy list of what needs to be cleaned out.

(Thanks to Skamradt for showing me this trick.)

Mason Wheeler
The only way for .dproj files to get corrupted that I have ever seen is editing your .dpr file manually, in particular deleting files from there. Just don't do that and you will be fine. Use the project manager's context menu instead.
dummzeuch
+16  A: 

I usually crucify programmers that don't put begin on a newline. That messes too much with my ability to read the code. Senior programmer's privilege. For the rest I'm pretty lax, stylewise.

Warning: Some open doors below:

  • DON'T with
  • DO learn about the unit system. It is undervalued, but strong.
  • DON'T Delphi widestring, if it can be avoided. It is a COM type. (on D2009, (unicode)string is more like the Kylix widestring, refcounted, not COM )
  • DON'T make a religion out of OOP. A global var can be just fine without a full unit to wrap it in a singleton.
  • DO learn how to instantiate runtime. The designer is a tool, nothing more.
  • DO visual inheritance, but keep it clean and simple.
  • DO implement patterns, but do it sanely and don't try to use 4 patterns in what is essentially a straight for loop.
  • DON'T hardcode any full paths. If you have to, always do this relative
  • DO always try to get source for your components
  • DON'T use the exception using versions of strto* if you can use trystrto*
  • DO always validate input
  • DON'T obfuscate code to hide a warning. Warnings are important, but just, euh warning. No need to carve up your code.
  • DO comment and use sane identifiers.
  • DO use doc tools that keep your documentation out of source. If your docs grow, your sources will be come unreadable.
  • DON'T try to use every new feature of your brand new version. There is a reason it was only just added. (exception: inlining, unicode, generics. I'm more talking about for-each etc)
  • DON'T use foreach for anything except iterating over an anonymous set (*)

For multi platform use and to future proof against future Delphi changes:

  • DO use a central includefile (http://www.stack.nl/~marcov/porting.pdf contains an article about that that never made the presses). ALWAYS include it just after the interface line.
  • DON'T use unit libc (FPC)
  • DO try to keep up with the status of units and functions/classes. Portability, if they are likely to be deprecated etc. If there is a constant predefined, use it, and don't roll your own (e.g. linebreak)
  • DO not overdo unitname IN 'path/to/OtherUnitName'; It makes it hard to relocate the source to a different path, and there is always a higher risk of duplicate filenames
  • DO make a good difference between integers and pointers. Use Ptrint/Ptruint (or intptr uintptr) for integers that scale with pointers.
  • DO not use TComponent.TAG for pointers.

(*) when are we finally going to get high and low for sets, so I can kill this exception?

Added later:

  • Be careful with adding RTTI to your classes. RTTI blows up binary sizes (not only because the sizeof the RTTI, but also because more code is reachable and can't be smartlinked). In recent forum.cg threads, there was some pressure to "improve" RTTI in future Delphi's. If so, see if there is a way to limit the scope of RTTI generation, or your binary size will explode. (D2010 does this, and has ways of limiting the scope) RTTI is nice, but needs a bit of management.
Marco van de Voort
What's wrong with for..in ? It makes for much cleaner code than for..to loops, and if you declare your enumerators as records and mark the working methods inline it makes the performance hit negligible. And I'd much rather write (and read!) "for myObject in myObjectList \ MyObject.DoSomething;" than "for i := 0 to MyObjectList.Count - 1 do \ (MyObjectList[i] as TMyObjectClass).DoSomething;"
Mason Wheeler
Well, a bit like with. Syntactic sugar without clear benefits, and I don't like duck typing. It is not strong typed, and IMHO goes against Pascal philosophy
Marco van de Voort
How is it not strongly typed? You have to declare your enumerator as a variable with a type, and it behaves just like any other variable with a type. I must be missing something.
Mason Wheeler
Well, you are right there. It might be only duck typing, but is still strong typed. Still I don't like it. It is superfluous.
Marco van de Voort
what is the alternative for widestring for non-delphi2009 users?
avar
MBCS. Or simply seriously limiting the unicode support till going to 2009.
Marco van de Voort
Nothing wrong with With in my book, much better than thing.up = thing.down = thing.left = thing.right =thing.smells =thing.eats =thing.etc =
Peter Turner
Prudent use is not that evil
Marco van de Voort
What do you mean by "learn about the Unit system"?
Larry Lustig
How it works, how to bring some structure into your units and imports (uses clauses and their location), how to disambiguate duplicate identifiers, type aliasing to reduce imports
Marco van de Voort
What is "type aliasing to reduce imports"? And I use units, but I'm afraid I probably don't put as much thought into their structure as I would, for instance, into a class hierarchy. Tips or reference for ideas on how to structure them?
Larry Lustig
A type alias is "type A=B;" it makes the types equivalent (in theory type A= type B; creates incompatible types, but that doesn't always work anymore). Say you have A unit a and B. A uses one or two global types from B, and expose them in e.g. methods. Now you have to import both A and B to use the classes from A. A few strategic aliases in A ( TYPE XXX = B.XXX;) avoids importing B in every unit that uses A.
Marco van de Voort
+7  A: 

Ok, at the risk of getting flamed down, I think somebody has to defend the poor "with" statement, for the sake of discussion. It seems to be the latest thing to bash "with". Damnit, I'm so out of fashion.

Somebody suggested to never ever use "with". To me, that seems to be a bit too paranoid.

Along the same logic, you should never ever use units or classes or records either. You might get a name-conflict/confusion with that too. Consider this example:

unit Unit1;    
interface    

uses  Windows; // contains a function called "LoadModule(...)"

type
  Windows=class
    class procedure LoadModule(...);
  end;

implementation

class procedure Windows.LoadModule(...);
begin
  // format c: here
end;

initialization
  Windows.LoadModule(...);

end.

Oh no! Name-conflict/confusing code!

What does this code do? Load a module, or format my harddrive?

Well, who knows.. But would you advice never ever to use multiple units or create classes because somebody could create a naming conflict or create confusing code?

My advice do use it when appropriate, but use it "with" care.

Wouter van Nifterick
To do that, you had to explicitly create a class whose name flagrantly violates Delphi convention. Yes, it can be done, but it has to be done on purpose. With lets you screw your code up accidentally, and makes it far too easy to do so. Also, it makes it harder to debug because the debugger's parser can't resolve with-ed identifiers.
Mason Wheeler
It's not that WITH creates nameconflicts. It is more that the gains are not worth the risk. It has been debated on hundreds of threads over the last decades, and the outcome is nearly always the same.I doubt everybody will make such case for classes.
Marco van de Voort
I know all the arguments againt it. "With" just comes in handy once and a while. You just have to know what you're doing because it's easy to shoot yourself in the foot with it. We've probably all been programming pascal since the TP days, and so far I've personally never had any serious problem because of a with statement... Just to put things in perspective.
Wouter van Nifterick
Although I am not so strictly against "with" than Mason, I have to agree with him, that debugging with-ified code is a nightmare
iamjoosy
+16  A: 
  1. As a Delphi 2009 user turn off the StringChecks option in the project settings. This causes an immense performance loss and code bloat if it is on (what it is by default). It is only needed due to the lack of a C++Builder unicode migration tool, but all Delphi users have to pay for it.

  2. Don't use "raise E;"

    on E: Exception do
    begin
      ...
      raise E;
    end;
    

    This will lead to an access violation because E is destroyed in the "end;". Change the code to "raise;" which calls the RTL function System.RaiseAgain instead of System.RaiseExcept(E)

  3. Use "const" for managed types (string, dynamic array, variant, interface). This will increase the speed of your application.

  4. Don't call the constructor after "try" because the variable will be uninitialized in the "finally" if the constructor throws an exception. Alternatively you can assign "nil" to the variable before the "try" and then call the constructor in the "try".

  5. Don't use strings for buffers. Strings are String and not Byte-Arrays. Especially if you plan to migrate your code to Delphi 2009 where SizeOf(Char) <> SizeOf(Byte).

  6. Don't combine "not" with a relation ("if not i <> 2") because "not" has a higher evaluation priority and will execute a bitwise not before the relation is evaluated resulting in odd results. Furthermore every relation operator has its counter-operator which makes the usage of "not" unnecessary.

  7. Use the overloaded Date/Time/Float functions with a self defined FormatSettings parameter if you write or read from files. If one of your customers use different format settings (e.g. "," as decimal separator), your application will fail otherwise.

Andreas Hausladen
For really brave older Delphi users, a {$implicitexceptions off} in the right place can do wonders too.
Marco van de Voort
+1, really great stuff
Peter Perháč
@Marco: But $implicitexceptions is only available for FPC. Delphi doesn't have this switch.
Andreas Hausladen
@Andreas: My bad. I expected D. to have something similar, since it has its runtime written in itself. Where it is an handy speedup
Marco van de Voort
+1  A: 

Be careful with abstractions and refactoring and, as Marco put it, don't make a religion out of it. Higher abstraction levels make code easier to write, but that's usually at the price of making it harder to debug, since you have more layers to dig through to get at what's actually happening. Use extra abstraction when it makes sense, not just whenever you can.

Mason Wheeler
In general, try to put yourself in the position of the guy that comes after you.
Marco van de Voort
another rule here, would be to DOCUMENT the abstraction.
skamradt
If it needs to be documented, the code is too complex.
dummzeuch
I can't agree with that one, dummzeuch. What seems intuitively obvious to you won't necessarily to the guy after you.
Mason Wheeler
+4  A: 

Don't use "inherited;"

It can cause subtle glitches if there isn't actually a method with the same signature in an ancestor class. It's better to state your intentions explicitly: inherited Create(AParameter);

(And yes, some people say it's good to use the naked inherited command because in certain situations involving abstract methods it can get you into trouble. But as long as we're explicitly stating our intentions here, the best solution is to just not put an inherited call in those methods. Perhaps you could even add a comment: "//do not call inherited here because..."

Mason Wheeler
Another bad thing about plain "inherited;" is that you can't ctrl-click it.
dangph
100 % agreed. Just 2 days ago that one hit me in Delphi's own Generics.Collections (Delphi 2009):
iamjoosy
constructor TObjectList<T>.Create(AOwnsObjects: Boolean);begin inherited; // this one is never called !!! FOwnsObjects := AOwnsObjects;end;
iamjoosy
+9  A: 

whenever it is possible use FreeAndNil instead of object.free for more refer to Why should you always use FreeAndNil instead of Free.

Do you know that FreeAndNil accept uninitialized variables? The compiler won't be able to tell you that the variable that you are going to free isn't initialized. And the result is an access violation.
Andreas Hausladen
i can't imagine some could pass uninitialized/uncreated object to freeandnil :-)
avar
oh, I certainly CAN :D Using FreeAndNil saved my life once. And I am using it ever since.
Peter Perháč
Using FreeAndNil on something that is not an object will result in weird errors. Eg. try to use it on an interface or a record. It will still compile but at runtime it will bomb out.
dummzeuch
+2  A: 

Do use proper names for variables which by looking at them one can easily tell what its for. If there would be any questions, then a small comment goes along way to saving an enormous amount of time later.

skamradt
A: 

DON'T use Data Modules, DO use a form instead and make it invisible after debugging.

PA
Just out of interest, what would be your reasoning in this?
Christopher Chase
I'm really curious about that. I heavily use DataModules and it make things A LOT easier for me. I agree with Christopher and would like to know why this....
Fabricio Araujo
Why in the world would you do that? Datamodules were specifically designed to be lighter weight than a standard form and to only be visible at design time. Why add the extra overhead of a full form that you don't intend to display?
Ken White
DON'T: follow this guy's advice, because it's the opposite of correct.
Tim Sullivan
Both DataModules and Forms can be used as containers for data related components.During development: Forms give you the opportunity to make parts of them visible, making easy to develop those components. After development: turning the form invisible, you have the very same container features than a datamodule has, but with a bonus, eventually you can turn them visible for problem determination.
PA
Since data related components are invisible anyway your point is moot. I do wonder how many more downvotes you want to collect for this silly answer?
mghie
the form would contain data grid components to allow you during development to interact with the data.
PA
i've done it and still i'm doing it for all of my applications, it has saved me hours of development time
PA
DataModules have only OnCreate and OnDestroy events, which makes them very clear and light weight. I use them a lot. And create them at run time, instead of making a separate component for some functionality. I will not vote you down for I think you've been punished too much already.
Mihaela
Clear and lightweight, but always invisible. Paying a little overhead (very little actually) gives you a lot of flexibility at development time by providing a visible interface to your data objects.
PA
+21  A: 

Install GExperts. You may never use anything but their grep, but for that alone, it's worth it: fast, flexible, and a great UI for viewing the search results. It's a pity their regular expression support sucks so bad.

Always make your constructor's first line be "inherited Create(...)" and your destructor's last line be "inherited Destroy;". Don't try to get tricky and do things before inherited Create, or (worse yet) after inherited Destroy; it will bite you sooner or later. Call the inherited constructor/destructor even if you're descending from TObject and the inherited call isn't technically necessary. You may refactor later, and not be descending directly from TObject anymore, and wondering why you have memory leaks.

When you're trying to decide whether something should be a function or a read-only property, ask yourself, "Is this something I would want to hover over in the debugger and see its value?" If so, it's worth the extra overhead to make it a property.

Learn to use the full-debug-mode version of the FastMM memory manager.

Understand that Free and FreeAndNil do nil checks for you. You don't need to write "if Something <> nil then FreeAndNil(Something)"; just call FreeAndNil.

Write your destructors defensively. In particular, don't assume that all your fields have been initialized, even if they're all initialized in the constructor. If an exception got thrown during the constructor, then the destructor will run immediately, and some of your fields might still be nil. If your destructor throws an access violation, you just lost out on finding out what the original error was.

Never optimize unless you've proven you know where the bottleneck is. (Not Delphi-specific, but relevant given some of others' tips about making parameters const and the like.)

In keeping with the previous point, never mark an interface-type parameter as "const" unless you really, really know you need to, with the profile results to back it up. It's too easy (and perfectly reasonable!) to write code that calls MyMethod(TMyInterfacedObject.Create). If MyMethod's parameter was marked as const, you just leaked an instance of TMyInterfacedObject -- the const parameter tells the compiler "don't bother calling _AddRef and _Release", so the object never got freed.

Avoid parallel indexed properties (e.g. a Strings[] property and an Objects[] property where Strings[3] logically corresponds to Objects[3]). Instead, make one indexed property that contains an object with multiple properties. Centralizes your logic and makes the code cleaner in the long run.

Don't use primitive data types if they don't fit. Make your own custom data types using records with methods. For example, if you keep needing to operate on a month-and-year (but don't care about the day), don't try to pass a pair of integers all over the place, or a TDateTime with a day that you try to ignore. Make a TMonthAndYear instead. Give it constructors and/or factory methods. Give it properties. Overload the = operator. Add an overloaded CheckEquals to your base DUnit test case that can compare two TMonthAndYears. You'll be amazed at how much clearer your logic becomes.

Last but not least: I know there are those who would rather be programming in assembly than in Delphi, would rather write their own memory manager than use FastMM, cringe in terror at the very mention of garbage collection, and who will therefore flame me endlessly for this, but our shop's #1 Delphi tip, arrived at through years of experience, is: Unless you have a good reason not to, interface everything. Don't waste brain cycles thinking about memory management; it is not worth it. Perhaps 0.5% of our interfaces have become performance problems, and (with profile results) we've been able to address those. And they make coding so much easier. Make a function that returns an interface instead of populating half a dozen out parameters, and use that result all you want but never have to free it. Get rid of the inevitable try .. finally .. FreeAndNil, and those endless chains of FreeAndNils in your destructors (did you remember them all?). Pass an object around with confidence that it'll get cleaned up when the last person is done with it. And curse the Delphi developers for forcing you to choose between code that's easy to browse through and code that's easy to maintain.

Joe White
+1, my favourite answer so far.
mghie
+1 too, i wish i could up it more
avar
btw I usually grep and find using cygwin.
Marco van de Voort
Could you flesh out and give a concrete example of your hint "interace everything"?
Larry Lustig
Basically, I mean that every time you write a class, you should also write an interface for its public methods, and never declare variables of the class type. Construct it, immediately assign it into an interface variable, and use it via the interface. Interfaces are refcounted, so all the try/finally/Free noise goes away. At first I didn't think this would make a big difference, but it does. I noted a few of the benefits in my answer, but it boils down to this: you can write more expressive code.
Joe White
Actually, I didn't got to this level but when I have to manage memory, I use a function to create a interface reference for my dynamically created objects. Usually they are TStringLists or Clientdatasets created on the fly. When the ref go out of scope, they die too. I call it Guarda (portuguese for 'Keep')... ;-)
Fabricio Araujo
A: 
  • Don't raise exceptions on destructors. Without destructor exceptions, you can free many objects without try except blocks, and your destructors you be more clean, without memory leaks.
  • When you need to free some resource(object, file, etc) after you call some function or method, use a try finally block even if this method don't raise exceptions. It's more safe, because some refactoring may change this function behaviour.
  • When call methods with var or out parameters, include a comment before the parameter. This make your code more readable.

    Ex:

    obj.method(param1, {var}param2);

  • When you destroy an object, call FreeAndNil, except on destructors(it's not necessary).

Marcelo Rocha
+3  A: 

DO: Unit test. Take some time to become familiar with DUnit, and use it as much as you can. Not only will it make your code better, but you'll start designing your code in a better way in order to facilitate testing.

Tim Sullivan
I prefer spending a lot of time on the testing of some central piece, rather than drumming up "mandatory" tests so that every bit and piece of code is "tested" by the book. Another thing to not turn into a religion
Marco van de Voort
You don't need to impose anything in order to do some unit testing. TDD is really useful when creating some object to tie up to your interface later, for example.
Leonardo Herrera
+1  A: 

i might add this tip : don't "eat" exceptions like

try
 SomeRoutineThatSometimesCausesAHardToFindAccessViolation
except    
end;

reference : Exception Handling for Fun and Profit

avar
AFAIK, this is called swallowing exceptions.... It's find to google that way ;-)
Fabricio Araujo
It's also called "Pokemon Exception Handling."
afrazier
+2  A: 
+5  A: 
  1. Do destroy a list of object in reverse order:

    for i := aList.Count -1 downto 0 do aList[i].free;

  2. Do use regular expressions for validating complex string input values. There's a great TPerlRegEx component. I highly recommend RegexBuddy which generates TPerlRegex code automatically.

  3. Use subversion for version control and synchronizing between multiple computers (laptop/desktop).

  4. Make InnoSetup project with all your current projects and third party components to recover quickly from HDD failure.

  5. Never buy a component without source code.

  6. Instead of buying Delphi Architect or Enterprise, buy a Professional edition and treat yourself with DevExpress VCL subscription and REMObjects. (This only applies if you don't need what Enterprise and Architect editions offer. I don't want to suggest that Delphi is expensive, because with all the functionality that you get with it, native code, large community and all, it's not, just the opposite. I'm switching to the Enterprise from the Professional this month.)

  7. Use conditional breakpoints to speed up your debugging and testing.

  8. Always use FastMM FullDebug Mode while developing your application too look for possible memory leaks as you add new functionality.

  9. Always use exception catching tool such as EurekaLog (my choice) or MadExcept, while developing. I even deploy my applications with it and integrate it with my FogBugz account.

  10. Use a code profiler. AQtime if you can afford it. I really like ProDelphi. Inexpensive but alters your code. If you choose it just backup your project, profile and restore. It's very precise.

Mihaela
Could you explain why 1. would be better than the other way? The only circumstance I could imagine would be the destructor removing the destroyed object from the list itself, and this seems pretty far-fetched for a general tip.
mghie
When you free an object from a list you also want to delete it from that list as well. Delete the pointer that pointed to it.So the code would be :for i := 0 to FList.Count -1 do begin FList[i].Free FList.Delete(i);end;Since Count is dynamically calculated it would decrease in each iteration.You're freeing 3 objects and deleting them from the list;1) Count = 3, i = 0, Free(0) OK2) Count = 2, i = 1, Free(1) OK3) Count = 1, i=2, Free(2) --> there is no element at index 2, and you get "List index out of bounds(2)" exception,
Mihaela
When you iterate from top of the list that does not happen.
Mihaela
@mghie: "this seems pretty far-fetched for a general tip." I disagree. This approach should become a habit because of what I demonstrated in a comment above. So that above should never happen. Destroying a list of object should be an automatic typing event, and not something one would have to think about. This way it always works.
Mihaela
No, it doesn't make sense. I don't know anyone who would remove the items from the list in that way, but if you would want to do it you could use a while loop, testing that Count is still greater than 0. But normally one would free all items in a loop, and use the Clear method after the loop. A reverse-iterating loop does always look strange IMHO, and does therefore need a comment why it is there. Better to stay with the idiomatic loop constructs.
mghie
Hi, I disagree. Otherwise the reserved word "downto" would not be in the language form the beginning, but just recently introduced. What if you just want to delete last few elements of the list. You could not call Clear afterwards.
Mihaela
Why would you want to remove an object from the list and not remove the pointer to it? I'm not saying that that's what you explicitly said, but just that it's implied from your approach.
Mihaela
Depends on the list, because you want to destroy the object and leave the rest of the cleaning to the inherited methods. For stringlists is inclusive faster doing this way, as Clear would clean the array much faster using Finalize and setting capacity to 0. Destroying the objects is the only thing that the inherited methods doesn't do.
Fabricio Araujo
In Delphi 2010, TStringList has an OwnsObjects property just like TObjectList. If you set that to True or pass True to the constructor, TStringList.Clear will free the objects too.
Jan Goyvaerts
Delphi Professional + RemObjects (and/or AnyDAC, since this is not included anymore) is sound advice.
Leonardo Herrera
+2  A: 
  • DON'T: use TStringList for everything.
  • DO: always write DisableControls, BeginUpdate before you populate or change control properties and EnableControls, EndUpdate after that.
  • DO: always use FastMM to track memory leaks. Even if you think that nothing leaks in your code.
  • DO: use multithreading if possible. Threads can improve your application performance drastically and you won't freeze GUI.
  • DO: use version control for your projects. Once you'll start with it, you'll never go back.
Linas
Ah, but TStringList is so useful! Actually with the new generic collections there are more useful alternatives. . . . I would suggest that overuse of multithreading when not necessary is a DON'T. SVN is better than Starteam, SourceSafe, but a lot of people are moving from it to GIT.
Jim McKeeth
I didn't mean using only SVN, so I edited my post.
Linas
SourceSafe get a lot of bashing - I never found someone talking something that's not negative about it. I've used StarTeam and, at least for a little team, does it's job well. And Change Requests work very close to the model used in QC, so it's very easy to use.
Fabricio Araujo