Spatial sp data are S4 class objects and are made up of slots (called using @) that contain components of the spatial feature class being represented (e.g., @data contains attributes, @coords contain coordinate pairs, etc...). You can return the top level slot names using slotNames() but it is not recursive and will not return nested slot names for polygon class objects. Each slot can contain a different object class and, before operating on it, should be checked using str() or class(). The @data slot is always a data.frame object and @coords is a matrix whereas @polygons is a list object with additional slots (labpt, area, hole, ringDir and coords).
The available slots and organization of them is dependent on what type of feature class is being represented. SpatialPointsDataFrame objects are the most basic, whereas SpatialPolygonsDataFrame objects have nesting (as seen above). This nested structure, representing each polygon, has to be accounted for using something like sapply to operate on each list object (polygon).
Here is an example that uses sapply to return the area for each polygon by iterating through the "polygons" then, the nested, "area" slot(s).
sapply(slot(sdat, 'polygons'), function(i) slot(i, 'area'))
In the case of polygon objects, since they are stored as a list for each polygon, you can alternatively use list indexing. Here is an example to return the first polygon (resulting in a "Polygon" class object and not SpatialPolygonsDataFrame):
In more recent versions of sp the developers have started, in some instances, removing the necessity of calling the @data slot directly.
For instance, to index @data you previously:
sdat@data[sdat@data$att >= 0.5 ,]
sdat[sdat$att >= 0.5 ,]
However, as previously indicated, this is not the case for the other slots (e.g., coordinates, polygons, etc...). As far as when to use  or $ this is still dependent on the type of operation. Brackets "" can be used to call a name in a dataframe but are primarily used for indexing whereas $ is specifically used to call a column in a dataframe. The reason that an "indirect" call to a column name works that the developers have added functionality to allow for a recursive search through the sp object. However, to avoid name conflicts (as in your example; having x,y columns in your dataframe would conflict with the x,y names in the @coord matrix names) there is some internal consistency checking that accounts for why this only works in some instances.
One convenient characteristic is that you can subset a spatial object through a row index. Here I am subsetting the first 10 objects.
sub.sdat <- sdat[1:10,]
Or, alternatively, a random sample (n=10) using a row index vector.
rs.sdat <- sdat[sample(1:nrow(sdat), 10),]
Understanding indexing and how to use brackets is a very important thing in writing R code.
Edit (03/24/2017): Please note that the simple feature (sf) class, following the GeoJSON standard, will likely become the new standard for spatial objects in R. You can read a detailed description of this class at the CRAN sf website Simple Features for R.