
In the framework of Mobile Camp Yandex, our colleague
Denis Lebedev presented a report on the new programming language Swift. In his report, he touched upon the features of interaction with Objective-C, spoke about the features of the language that seemed to him the most interesting. And also about where to go on Github, and what repositories to look at in order to understand what can be done with Swift in the real world.
Swift development began in 2010. Engaged in her Chris Lattner. Until 2013, the process was not very active. Gradually more people were involved. In 2013, Apple focused on developing this language. Before the presentation at WWDC about Swift knew about 200 people. Information about him was kept in the strictest confidence.
Swift is a multi-paradigm language. It has OOP, you can try some functional things, although adherents of functional programming believe that Swift is not quite enough. But, it seems to me that such a goal was not set, after all, it is a language for people, not for mathematicians. You can write in procedural style, but I'm not sure that this is applicable for entire projects. Very interesting in Swift everything is arranged with typing. Unlike dynamic Objective-C, it is static. There is also a type inference. Those. most variable type declarations can be simply omitted. Well, the killer feature Swift can be considered a very deep interaction with Objective-C. Later I will talk about runtime, but for now we’ll restrict ourselves to the fact that the code from Swift can be used in Objective-C and vice versa. Pointers familiar to all developers on Objective-C and C ++ in Swift are absent.
')
Let's pass to features. There are a lot of them. I singled out for myself a few basic ones.
- Namespacing. Everyone understands the problem of Objective-C - because of the two-letter and three-letter classes, name collisions often occur. Swift solves this problem by introducing obvious and understandable names for all. While they do not work, but everyone should be fixed for release.
- Generic classes & functions. For people who have written in C ++, this is quite an obvious thing, but for those who have come across mainly Objective-C, this is a fairly new feature that will be interesting to work with.
- Named / default parameters. You won't surprise anyone with the named parameters, they already were in Objective-C. But the default settings are a very useful thing. When our method takes five arguments, three of which are set by default, the function call becomes much shorter.
- Functions are first class citizens. Functions in Swift are first order objects. This means that they can be passed to other methods as parameters, as well as returned from other methods.
- Optional types. Optional types are an interesting concept that came to us in a slightly modified form from functional programming.
Consider the last feature in a little more detail. We are all used to that in Objective-C, when we do not know what to return, we return nil for objects and -1 or NSNotFound for scalars. Optional types solve this problem quite radically. An optional type can be represented as a box that either contains a value or contains nothing. And it works with any types. Suppose we have the following signature:
(NSInteger) indexOfObjec: (id)object;
In Objective-C, it is unclear what the method returns. If there is no object, then it can be -1, NSNotFound or some other constant known only to the developer. If we consider the same method in Swift, we will see Int with a question mark:
func indexOF(object: AnyObject) -> Int?
This construct tells us that either the number or the void will return. Accordingly, when we received the packed Int, we need to unpack it. Unpacking can be of two types: safe (everything turns into if / else) and forced. We can use the latter only if we know for sure that there will be values in our imaginary box. If it does not appear there, there will be a crash in rantayma.
Now let's talk briefly about the main features of classes, structures and enumerations. The main difference between classes and structures is that they are passed by reference. Structures are passed by value. As the documentation tells us, using structures spends far less resources. And all scalar types and Boolean variables are implemented through structures.
Enumerations I would like to highlight separately. They are completely different from their counterparts in C, Objective-C and other languages. It is a combination of class, structure and even a bit more. To show what I mean, consider an example. Suppose I want to implement a tree using
enum
. Let's start with a small listing with three elements (empty, node and sheet):
enum Tree { case Empty case Leaf case Node }
What to do with this is unclear. But in Swift, each
enum
element can carry some value. To do this, we add
Int
to the leaf, and the node will have two more trees:
enum Tree { case Empty case Leaf(Int) case Node(Tree, Tree) }
But since Swift supports generics, we will add support of any type to our tree:
enum Tree<T> { case Empty case Leaf(T) case Node(Tree, Tree) }
The tree declaration will look something like this:
let tree: Tree<Int> = .Node(.Leaf(1), .Leaf(1))
Here we see another cool feature: we can not write the names of enumerations, because Swift displays these types at the compilation stage.
enum
in Swift has another interesting feature: they can contain functions, just as in structures and classes. Suppose I want to write a function that returns the depth of our tree.
enum Tree { case Empty case Leaf(Int) case Node(Tree, Tree) func depth<T>(t: Tree<T>) -> Int { return 0 } }
What I don’t like about this function is that it takes a tree parameter. I want to make the function just return values to me, and I would not need to transfer anything. Here we will use another interesting Swift feature: nested functions. Since there are no access modifiers yet - this is one of the ways to make the function private. Accordingly, we have
_depth
, which will now count the depth of our tree.
enum Tree<T> { case … func depth() -> Int { func _depth<T>(t: Tree<T>) -> Int { return 0 } return _depth(self) } }
We see a standard switch, there is nothing sviftovogo here, just handle the option when the tree is empty. Further interesting things begin. We unpack the value that is stored in our list. But since we do not need it, and we just want to return the unit, we use an underscore, which means that we do not need a variable in the sheet. Next we unpack the knot from which we take the left and right parts. Then we call the depth function recursively and return the result. As a result, we get such a tree implemented on
enum
with some basic operation.
enum Tree<T> { case Empty case Leaf(T) case Node(Tree, Tree) func depth() -> Int { func _depth<T>(t: Tree<T>) -> Int { switch t { case .Empty: return 0 case .Leaf(let_): return 1 case .Node(let lhs, let rhs): return max(_depth(lhs), _depth(rhs)) } } return _depth(self) } }
The interesting thing about this
enum
is that this code written by him should work, but it does not work. In the current version, because of the
enum
bug, it does not support recursive types. In the future, it will work. For the time being, different hacks are used to bypass this bug. I will tell about one of them a bit later.
The next item in my story is collections, represented in the standard library by an array, dictionaries, and a string (enchantment collection). Collections, like scalars, are structures, they are also interchangeable with standard foundation types such as NSDictionary and NSArray. In addition, we see that, for some strange reason, there is no NSSet type. Probably used too rarely. Some operations (for example,
filter
and
reverse
) have lazy calculations:
func filter<S :Sequence>(…) -> Bool) -> FilterSequenceView<S> func reverce<S :Collection …>(source: C) -> ReverseView<C>
Those.
FilterSequenceView
and
ReverseView
types are not a processed collection, but its representation. This tells us that these methods have high performance. In the same Objective-C, such tricky constructions cannot be found, since at the time of the creation of this language no one had yet thought about such concepts. Now lazy calculations penetrate programming languages. I like this trend, sometimes it is very effective.
The following feature was noticed, probably, by everyone who was somehow interested in the new language. But I still tell you about it. Swift has built-in variable immutability. We can declare a variable in two ways: through
var
and
let
. In the first case, the variables can be changed, in the second - not.
var = 3 b += 1 let a = 3 a += 1
Here begins an interesting thing. For example, if we look at a dictionary that is declared using the
let
directive, then when we try to change a key or add a new one, we will get an error.
let d = ["key": 0] d = ["key"] = 3 //error d.updateValue(1, forKey: "key1") //error
Arrays are different. We cannot increase the size of the array, but at the same time we can change any of its elements.
let c = [1, 2, 3] c[0] = 3
In fact, it is very strange, when trying to figure out what was happening, it turned out that this was a bug confirmed by the developer of the language. In the near future, it will be corrected, because This is really a very strange behavior.
Extensions in Swift are very similar to categories from Objective-C, but more penetrate the language. In Swift, you do not need to write imports: we can write an extension anywhere in the code, and it will be picked up by absolutely all the code. Accordingly, in the same way it is possible to expand structures and enams, which is also sometimes convenient. With the help of extensions, you can structure the code very well, it is implemented in the standard library.
struct: Foo { let value : Int } extension Foo : Printable { var description : String { get {return "Foo"} } } extension Foo : Equatable { } func ==(lhs: Foo, rhs: Foo) -> Bool { return lhs.value == rhs.value }
Next, let's talk about what in Swift is not. I can not say that something concrete is missing for me, because I haven't used it in production yet. But there are things that many complain about.
- Preprocessor. It is clear that if there is no preprocessor, then there are no cool macros that generate a lot of code for us. Cross-platform development is also hampered.
- Exceptions. The mechanism is completely missing, but you can create an NSException, and the Objective-C runtime will handle all this.
- Access control. After reading the Swift book, many were confused by the lack of access modifiers. In Objective-C, this was not, everyone understood that it was necessary, and waited in the new language. In fact, the developers simply did not have time to implement the beta access modifiers. In the final release they will be.
- KVO, KVC. For obvious reasons, there is no Key Value Observing and Key Value Coding. Swift is a static language, and these are features of dynamic languages.
- Compiler attributes. There are no compiler directives that report deprecated methods or whether there is a method on a specific platform.
performSelector.
This Swift method is completely mowed down. This is a rather insecure thing and even in Objective-C you need to use it with caution.
Now let's talk about how Objective-C and Swift can interfere. Everyone already knows that you can call Objective-C code from Swift. In the opposite direction, everything works exactly the same, but with some limitations. Enums, tuples, generic types do not work. Although there are no pointers, CoreFoundation types can be called directly. For many, the impossibility of calling C ++ code directly from Swift has become a frustration. However, you can write wrappers in Objective-C and call them already. Well, it is quite natural that it is impossible to subclass in Objective-C classes that are not implemented in it from Swift.
As I said above, some types are interchangeable:
NSArray < - > Array;
NSDictionary < - > Dictionary
;NSNumber - > Int, Double, Float
.
I will give an example of a class that is written in Swift, but can be used in Objective-C, you just need to add one directive:
@objc class Foo { int (bar: String) { } }
If we want a class in Objective-C to have a different name (for example, not
Foo
, but
objc_Foo
), as well as change the method signature, everything becomes a bit more complicated:
@objc(objc_Foo) class Foo{ @objc(initWithBar:) init (bar: String) { } }
Accordingly, in Objective-C everything looks absolutely expected:
Foo *foo = [[Foo alloc] initWithBar:@"Bar"];
Naturally, you can use all the standard frameworks. For all headers, their representation on Swift is automatically generated. Suppose we have a function
convertPoint
:
- (CGPoint)convertPoint:(CGPoint)point toWindow:(UIWindow *)window
It is fully converted to Swift with the only difference: there is an exclamation point near
UIWindow
. This indicates the very optional type I mentioned above. Those. if there is nil, and we do not check it, there will be crash in runtime. This is due to the fact that when the generator creates these headers, he does not know whether it can be nil or not, therefore he puts these exclamation marks everywhere. Perhaps soon it will be corrected somehow.
finc convertPoint(point: CGPoint, toWindow window: UIWindow!) -> GCPoint
In detail, it’s too early to talk about Swift's innards and performance, since it’s not known that it’s going to live from the current runtime to the first version. Therefore, for now let's touch this topic only superficially. To begin with, all Swift objects are Objective-C objects. There is a new root class SwiftObject. Methods are now stored not in classes, but in virtual tables. Another interesting feature is that the types of variables are stored separately. Therefore, it becomes a bit more difficult to decode classes on the fly. For coding method metadata, an approach called name mangling is used. For example, take a look at the
Foo
class with the
bar
method returning
Bool
:
class Foo { func bar() -> Bool { return false } }
If we look at the binary, for the
bar
method we will see a signature of the following form:
_TFC9test3Foo3barfS0_FT_Sb
. Here we have
Foo
with a length of 3 characters, the length of the method is also 3 characters, and
Sb
at the end means that the method returns
Bool
. A not very pleasant thing is connected with this: debug logs in XCode all fall into exactly this form, so it’s not very convenient to read them.
Probably everyone has already read about the fact that Swift is very slow. By and large this is true, but let's try to figure it out. If we compile with the
-O0
flag, i.e. without any optimizations, Swift will be slower than C ++ 10 to 100 times. If we compile with the
-O3
flag, we will get 10 times slower than C ++. The
-Ofast
flag
-Ofast
not very secure, since it disables inta checking, etc., in runtime. In production it is better not to use it. However, it allows you to improve performance up to the C ++ level.
You need to understand that the language is very young, it is still in beta. In the future, the main problems with speed will be fixed. In addition, the legacy of Objective-C stretches behind Swift, for example, in cycles there are a huge number of retainers and releases that are essentially not needed in Swift, but they slow down the speed very much.
Then I will talk about things that are not very related to each other that I encountered during the development process. As I said above, macros are not supported, so the only way to make a cross-platform view is as follows:
#if os(iOS) typealias View = UView #else typealias View = NSView #endif class MyControl : View { }
This
if
is not exactly a preprocessor, but simply a language construct that allows you to test the platform. Accordingly, we have a method that returns us to what platform we are on. Depending on this, we alias the
View
. So we create
MyControl
, which will work on both iOS and OS X.
The next feature - pattern matching - I really like. I'm a little fond of functional languages, where it is used very widely. Take for example the problem: we have a point on the plane, and we want to understand which of the four quadrants it is in. We all imagine what that code would be in Objective-C. For each quadrant, we will have such absolutely wild conditions, where we have to check whether x and y fall into these frames:
let point = (0, 1) if point.0 >= 0 && point.0 <= 1 && point.1 >= 0 && point.1 <= 1 { println("I") } ...
Swift us in this case gives us a few convenient pieces. First, we have a tricky range-operator with three points. Accordingly, the
case
can check whether the point falls into the first quadrant. And the whole code will look something like this:
let point = (0, 1) switch point { case (0, 0) println("Point is at the origin") case (0...1, 0...1): println("I") case (-1...0, 0...1): println("II") case (-1...0, -1...0): println("III") case (0...1, -1...0): println("IV") default: println("I don't know") }
In my opinion, this is dozens of times more readable than what Objective-C can provide us.
In Swift, there is another absolutely niche thing that also came from functional programming languages - function currying:
func add(a: Int)(b: Int) -> Int { return a + b } let foo = add(5)(b: 3) // 8 let add5 = add(5) // (Int) -> Int let bar = add(b: 3) // 8
We see that we have a function
add
with such a tricky declaration: two pairs of brackets with parameters instead of one. This gives us the opportunity to either call this function almost as usual and get the result of 8, or call it with one parameter. In the second case, magic happens: at the output, we get a function that takes an
Int
and returns an
Int
too, i.e. we partially applied our
add
function to the top five. Accordingly, we can then use the
add5
function with a triple and get a figure eight.
As I said, the preprocessor is missing, so even implementing
assert
is not a trivial thing. Suppose we have a task to write some kind of
assert
. We can check it for debag, but in order for the code that is not executed in the assertion, we must pass it as a closure. Those. we see that we have
5 % 2
in curly braces. In Objective-C terminology, this is a block.
func assert(condition:() -> Bool, message: String) { #if DEBUG if !condition() { println(message) } #endif } assert({5 % 2 == 0}, "5 isn't an even number.")
It is clear that no one will use asses. Therefore, there are automatic closures in Swift. In the method declaration, we see
@autoclosure
, respectively, the first argument turns into a closure, and you can
@autoclosure
curly braces.
func assert(condition: @auto_closure () -> Bool, message: String) { #if DEBUG if !condition() { println(message) } #endif } assert(5 % 2 == 0, "5 isn't an even number.")
Another undocumented, but very useful thing is the explicit type conversion. Swift is a typed language, so as in Objective-C we cannot stick objects with id-type. Therefore, consider the following example. Suppose I have a
Box
structure, which in gets at initialization some value that cannot be changed. And we have a packed
Int
- unit.
struct Box<T> { let _value : T init (_ value: T) { _value = value } } let boxedInt = Box(1)
We also have a function that takes an
Int
as input. Accordingly, we cannot transfer
boxedInt
there, since the compiler will tell us that
Box
not converted to an
Int
. The craftsmen slightly gutted the internal parts of the SWIFT and found a function that allows you to convert the
Box
type into a value that it hides in itself:
extension Box { @conversion Func __conversion() -> T { return _value } } foo(boxedInt) //success
Static typing of the language also does not allow us to run around the class and replace methods, as could be done in Objective-C. From what is now, we can only get a list of object properties and display their values at the moment. Those. information about the methods we can not get.
struct Foo { var str = "Apple" let int = 13 func foo() { } } reflect(Foo()).count
From the swift, you can directly call the C-code. This feature is not reflected in the documentation, but may be useful.
@asmname("my_c_func") func my_c_func(UInt64, CMutablePointer<UInt64>) -> CInt;
Swift, of course, is a compiled language, but this does not prevent it from supporting scripts. First, there is an interactive runtime environment that is run with the
xcrun swift
command. In addition, you can write scripts not in familiar scripting languages, but directly in Swift. They are
xcrun -i 'file.swift'
using the
xcrun -i 'file.swift'
.
Finally, I will talk about the repositories that are worth a look:
- BDD Testing framework: Quick . This is the first thing that everyone lacked. The framework is actively developing, new matchers are constantly being added.
- Reactive programming: RXSwift . This is a rethinking of ReactiveCocoa with the help of constructions provided by swift.
- Model mapping: Crust . Analog Mantle for Swift. Allows you to map JSON objects to swift objects. Used many interesting hacks that may be useful in development.
- Handy JSON processing: SwiftyJSON . This is a very small library, literally 200 lines. But it demonstrates the power of transfers.