当前位置:网站首页 > Rust编程 > 正文

rust编程指南_rust与c的性能

类型系统

基本类型

  • 基本类型
  • 复合类型
  • 作用域
  • 返回值
    • Option<T>
    • Result<T,E>

编程语言中不同的类型本质上是内存占用空间和编码方式的不同,Rust也不例外。

Rust中绝大部分类型都是在编译期可确定大小的类型(Sized Type),如u8,f64等

Rust也有少量的动态大小的类型(Dynamic Sized Type,DST),如str

对于动态类型,Rust提供了引用方式来解决,引用类型存在栈空间,具体内容存在堆空间。

例如:

image-20220222145340864

包含了动态大小类型地址信息和携带了长度信息的指针,叫作胖指针(Fat Pointer),&str就是胖指针。

有如下代码

fn test(mut arr:[u32]){ 
    arr[1] = 5; arr[2] = 1; } fn main(){ 
    let arr:[u32]= [1,2,4,5,6]; test(arr); } 

以上代码编译会出错

image-20220222150242057

表示期望切片类型,找到的是数组类型,做如下修改可通过编译

fn test(mut arr:[u32;5]){ 
    arr[1] = 5; arr[2] = 1; } fn main(){ 
    let arr:[u32;5]= [1,2,4,5,6]; test(arr); println!("{:?}",arr);// 1 2 4 5 6 } 

这样类型就匹配了,但是arr结果并没有被改变,其实是因为传入的参数通过shadow机制在栈空间创建了一个新的arr,所以并不会改变原来的值。可以类比C语言的值传递swap(a,b),结果并不会交换a,b。

c语言解决方法是传入a,b的指针,Rust也一样,传入可变引用,即胖指针即可

fn reverse(arr:&mut [u32]){ 
    arr[0] = 3; arr[1] = 2; arr[2] = 1; println!("len : {}",arr.len()); } fn main(){ 
    let mut arr= [1,2,3];//[u32;3] reverse(&mut arr);//传入的引用包含了大小,编译器解决 println!("{:?}",arr); } 

可以查看占用内容大小

fn main(){ 
    println!("{}",std::mem::size_of::<&mut [u32]>());//16 println!("{}",std::mem::size_of::<& [u32;3]>());//8 } 

胖指针由来。

ZST类型

除了可确定大小类型和DST类型,Rust还支持零大小类型(Zero Sized Type,ZST),比如单元类型和单元结构体,大小都是零。

以下都为ZST类型

enum void { 
   } struct foo; struct Baz{ 
    f:foo, q:(), buf:[u16;0], em:void, } fn main(){ 
    println!("{}",std::mem::size_of::<void>());//0 println!("{}",std::mem::size_of::<foo>());//0 println!("{}",std::mem::size_of::<Baz>());//0 println!("{}",std::mem::size_of::<[();32]>());//0 } 

底类型

如never类

zst是空,底类型是无。

  • 没有值。
  • 是其他任意类型的子类型
  • 用!表示

以下返回底类型:

  • 发散函数(Diverging Function)
  • continue和break关键字
  • loop循环
  • 空枚举,比如enum Void{}

以下代码正常执行

fn foo()->!{ 
    loop { 
    } } fn main(){ 
    let a = if false{ 
    foo() }else{ 
    100 }; print!("{}",a); } 

因为!是所有类型的子类型。

以下不可

enum Void { 
   } fn main(){ 
    let res:Result<u32,Void> = Ok(0); let Ok(num) = res; } 

Rust中使用Result类型来进行错误处理,强制开发者处理Ok和Err两种情况,但是有时可能永远没有Err,这时使用enum Void{}就可以避免处理Err的情况。

但是可惜的是,当前版本的Rust还不支持上面的语法,编译会报错。不过Rust团队还在持续完善中,在不久的将来Rust就会支持此用法。

类型推断

Rust支持类型推断,但其功能并不像Haskell那样强大,Rust只能在局部范围内进行类型推导

fn add(a:u32,b:u32)->u32{ 
    a+b } fn main(){ 
    let a = 1; let b = 2; add(a, b); } 

Turbofish操作符

fn main(){ 
    let x = "1"; print!("{:?}",x.parse::<i32>().unwrap()); } 

::<>就是Turbofish操作符

不完善

fn main(){ 
    let x = 0; let flag = x.is_positive(); } 

can’t call method is_positive on ambiguous numeric type {integer}

泛型

泛型(Generic)是一种参数化多态。使用泛型可以编写更为抽象的代码,减少工作量。简单来说,泛型就是把一个泛化的类型作为参数,单个类型就可以抽象化为一簇类型。

即把类型作为参数。

泛型可以用在,函数,结构体,使用泛型必须声明<T>

struct Point<T>{ 
    x:T, y:T, } fn get_x<T>(x:T) -> T{ 
    x } 

为泛型结构体实现方法impl<T>

struct Message<T>{ 
    content:T, } impl <T> Message<T> { 
    fn new(content:T)->Self{ 
    Message { 
    content } } } 

Rust中的泛型属于静多态,它是一种编译期多态。在编译期,不管是泛型枚举,还是泛型函数和泛型结构体,都会被单态化

(Monomorphization)。单态化是编译器进行静态分发的一种策略

泛型及单态化是Rust的最重要的两个功能。单态化静态分发的好处是性能好,没有运行时开销;缺点是容易造成编译后生成的二进制文件膨胀。这个缺点并不影响使用Rust编程。

返回值推导

struct Foo(i32); struct Bar(i32,i32); trait Instance { 
    fn new(i:i32) -> Self; } impl Instance for Foo { 
    fn new(i:i32) -> Foo { 
    Foo(i) } } impl Instance for Bar{ 
    fn new(i:i32)->Self{ 
   //Self指结构体本身 Bar(i,i+10) } } fn foobar<T:Instance>(i:i32) -> T{ 
   //表明T是实现了Instance trait的结构体 T::new(i) } fn main(){ 
    let f:Foo = foobar(10); let b:Bar = foobar(20); } 

深入Trait

可以说trait是Rust的灵魂。Rust中所有的抽象,比如接口抽象、OOP范式抽象、函数式范式抽象等,均基于trait来完成。同时,trait也保证了这些抽象几乎都是运行时零开销的。

从类型系统的角度来说,trait是Rust对Ad-hoc多态的支持。从语义上来说,trait是在行为上对类型的约束,这种约束可以让trait有如下4种用法:

  • 接口抽象。对类型行为的统一约束。
  • 泛型约束。泛型的行为被trait限定在更有限的范围内。
  • 抽象类型。在运行时作为一种间接的抽象类型去使用,动态地分发给具体的类型。
  • 标签trait。对类型的约束,直接作为标签使用。
接口抽象

特点:

  • 接口中定义方法,并支持默认实现。
  • 接口中不能实现另一个接口,但接口间可以继承。
  • 同一个接口可以被多个类型实现,但不能被同一个类型实现。
  • 使用impl关键字为类型实现接口方法。
  • 使用trait关键字定义接口。

    image-20220222164208172

trait T_A{ 
    fn say(msg:&str)->String; } struct A; struct B; enum C{ 
    } impl T_A for A{ 
    fn say(msg:&str)-> String{ 
    let t = "A".to_string(); t + msg } } impl T_A for B{ 
    fn say(msg:&str)->String{ 
    let t = "B".to_string(); t + msg } } impl T_A for C{ 
    fn say(msg:&str)->String{ 
    let t = "C".to_string(); t + msg } } fn main(){ 
    } 

加减乘除等也是一种trait

trait Add<RHS,Output>{ 
    fn my_add(self,rhs:RHS) -> Output; } impl Add<i32,i32> for i32 { 
    fn my_add(self,rhs:i32)->i32{ 
    self + rhs } } impl Add<u32,i32> for u32{ 
    fn my_add(self,rhs:u32) -> i32{ 
    (self + rhs) as i32 } } fn main(){ 
    let (a,b,c,d) = (1i32,2i32,3u32,4u32); let x:i32 = a.my_add(b); let y:i32 = c.my_add(d); assert_eq!(x,3i32); assert_eq!(y,7i32); } 

RHS是+的右侧值类型,Output是输出类型。

对于Add,加法的输入与输出值应该为同一类型,所以Add<RHS,Output>的output有点多余,于是有以下写法

pub trait Add<RHS =Self >{ 
   // 指定RHS的默认值为Self type Output; fn add(self,rhs:RHS) -> Self::Output; } 

type Output 叫关联类型,Self是每个trait都带有的隐式类型参数。

impl Add<&str> for String{ 
    type Output = String; fn add(mut self,other:&str) -> String{ 
    self.push_str(other); self } } 

trait一致性

可以实现操作符重载。

如想要实现u32+u64

use std::ops::Add; impl Add<u64> for u32{ 
    type Output = u64; fn add(self,other:u64) -> Self::Output{ 
    (self as u64) + other } } fn main(){ 
    let a = 1u32; let b = 2u64; println!("{}",a+b); } 

编译报错。

image-20220222185151593

这是因为Rust遵循一条重要的规则:孤儿规则(Orphan Rule)。孤儿规则规定,如果要实现某个trait,那么该trait和要实现该trait的那个类型至少有一个要在当前crate中定义

Add trait和u32、u64都不是在当前crate中定义的,而是定义于标准库中的。如果没有孤儿规则的限制,标准库中u32类型的加法行为就会被破坏性地改写,导致所有使用u32类型的crate可能产生难以预料的Bug。

解决如下,讲Add trait 定义在当前crate就可以了,当然可以不一定叫Add,和add()

 trait Add<RHS =Self >{ 
    type Output; fn add(self,rhs:RHS) -> Self::Output; } impl Add<u64> for u32{ 
    type Output = u64; fn add(self,other:u64) -> Self::Output{ 
    (self as u64) + other } } fn main(){ 
    let a = 1u32; let b = 2u64; println!("{}",a.add(b)); } 

对其他类型实现trait

 use std::ops::Add; #[derive(Debug)] struct Point{ 
    x:i32, y:i32, } impl Add for Point { 
    type Output = Self; fn add(self,other:Point) -> Self::Output{ 
    Point{ 
    x:self.x+other.x, y:self.y+other.y, } } } fn main(){ 
    let p1 = Point{ 
    x:1, y:2, }; let p2 = Point{ 
    x:2, y:3, }; println!("{:?}",p1+p2); } 

trait 继承

Rust不支持传统面向对象的继承,但是支持trait继承。子trait可以继承父trait中定义或实现的方法。在日常编程中,trait中定义的一些行为可能会有重复的情况,使用trait继承可以简化编程,方便组合,让代码更加优美。

例如web中常用的分页

trait Page{ 
    fn set_page(&self, p:i32){ 
    println!("Page Default 1"); } } trait PerPage{ 
    fn set_perpage(&self, num:i32){ 
    println!("Per Page Default 10"); } } struct MyPaginate{ 
   page:i32} impl Page for MyPaginate { 
   } impl PerPage for MyPaginate { 
   } fn main(){ 
    let my_paginate = MyPaginate{ 
   page:1}; my_paginate.set_page(2); my_paginate.set_perpage(100); } 

MyPaginate实现了两个trait。

如果要再实现一个跳转功能,可以使用继承

trait Paginate: Page + PerPage{ 
   //继承写法 :trait fn set_skip_page(&self,num:i32){ 
    println!("skip to page {}",num); } } impl <T:Page+PerPage> Paginate for T { 
   }//T为泛型,类型为实现了Page+PerPage的类型,整句话表示为实现了T的类型实现Paginate fn main(){ 
    let my_paginate = MyPaginate{ 
   page:1}; my_paginate.set_page(2); my_paginate.set_perpage(100); my_paginate.set_skip_page(12);//skip to page 12 } 

优点是添加了新功能再不影响之前功能的情况下。

trait约束

很多情况下,一个行为并不是为所有类型实现的。比如:

fn sum<T>(a:T,b:T)->T{ 
   //编译不通过 a+b } 

整型相加可以,字符串相加可以,但是整型与bool类型,就不行。

可以限制T为可加类型

use std::ops::Add; fn sum<T:Add<T,Output=T>>(a:T,b:T)->T{ 
   //限制T的类型为实现了Add的类型 a+b } fn main(){ 
    let a = 1; let b = 2; let c = sum(a, b); println!("{}",c); } 

使用trait对泛型进行约束,叫作trait限定(trait Bound)。格式如下

fn generic<T: MyTrait + MyOtherTrait + SomeStandTrait>(t:T){ 
   } 

泛型限定是许多语言都有概念,是Structural Typing的变种,Rust中的trait限定也是Structural Typing的一种实现。

也可以从数学的角度理解trait限定,例如

trait Paginate: Page + PerPage{} 

注意:

如果有trait A,B,C,A,B,C中的方法不能同名,

不能覆盖,

C:A+B,为某类型实现C必须要实现A和B。否则报错。

trait A { 
    fn getA(&self,i:i32){ 
    println!("A : {}",i); } } trait B { 
    fn getB(&self,i:i32){ 
    println!("B : {}",i); } } trait C:A+B { 
    fn getC(&self,i:i32){ 
    println!("C : {}",i); } } impl A for Test { 
   } impl B for Test { 
   } // impl C for Test {} impl <T> C for T where T:A+B { 
   } struct Test{ 
    a:i32, } fn main(){ 
    let test = Test{ 
   a:1}; test.getB(2); test.getA(1); test.getC(3); } 
抽象类型

相对于具体类型而言,抽象类型无法直接实例化,它的每个实例都是具体类型的实例。

对于抽象类型而言,编译器可能无法确定其确切的功能和所占的空间大小。所以Rust目前有两种方法来处理抽象类型:trait对象和impl Trait。

trait对象

use std::fmt::Debug; #[derive(Debug)] struct Foo; #[derive(Debug)] struct Fun; trait Bar{ 
    fn baz(&self); } impl Bar for Foo{ 
    fn baz(&self){ 
    println!("{:#?}",self); } } impl Bar for Fun{ 
    fn baz(&self){ 
    println!("{:#?}",self); } } fn static_dispatch<T>(t:&T) where T:Bar{ 
    t.baz(); } fn dynamic_dispatch(t:&dyn Bar){ 
   //动态分发 t.baz(); } fn main(){ 
    let foo = Foo; let fun = Fun; static_dispatch(&foo); static_dispatch(&fun); dynamic_dispatch(&foo); dynamic_dispatch(&fun); } 

动态分发

trait本身也是一种类型,但它的类型大小在编译期是无法确定的,所以trait对象必须使用指针。可以利用引用操作符&或 Box<T>来制造一个 trait 对象。trait 对象等价代码如下结构体

pub struct TraitObject{ 
    pub data: *mut (), pub vtable: *mut (), } 

TraitObject 在栈区,数据指针指向堆区数据部分,vtable 名称来自C++,保护了析构函数,大小,函数等信息。

image-20220224160807548

在编译器TraitObject不知道调用哪个方法,但是指针的大小确定,在运行器通过trait_object.method()可以知道函数的指针,然后进行调用。

类比java继承父类可以调用子类的实现。

并不是每个trait都可以作为trait对象被使用,这依旧和类型大小是否确定有关系。每个trait都包含一个隐式的类型参数Self,代表实现该trait的类型。Self默认有一个隐式的trait限定?Sized,形如<Self:?Sized>,?Sized trait 包括了所有的动态大小类型和所有可确定大小的类型。Rust 中大部分类型都默认是可确定大小的类型,也就是<T:Sized>,这也是泛型代码可以正常编译的原因。

必须同时满足以下两条规则的trait才可以作为trait对象使用

简单来说,要将trait作为trait对象就不加限制,否则就加上Sized

trait A:Sized{ 
    //code } 

安全的trait对象实例

 trait Bar{ 
    fn bax(self,x:u32); fn baz(&mut self); } 

不安全的trait对象实例

trait Foo{ 
    fn bad<T>(&self,x:T); fn new() ->Self;//Self是unsized } 
  1. 拆分
 trait Foo{ 
    fn bad<T>(&self,x:T); } trait Foo:Bar{ 
    fn new() ->Self;//Self是Bar继承的 } 
  1. 使用where限定
trait Foo{ 
    fn bad<T>(&self,x:T); fn new()->Self where Self:Sized; } 

impl Trait

在Rust 2018版本中,引入了可以静态分发的抽象类型impl Trait。如果说trait对象是装箱抽象类型(Boxed Abstract Type)的话,那么impl Trait就是拆箱抽象类型(Unboxed Abstract Type)。“装箱”和“拆箱”是业界的抽象俗语,其中“装箱”代表将值托管到堆内存,而“拆箱”则是在栈内存中生成新的值,目前impl Trait只可以在输入的参数和返回值这两个位置使用,在不远的将来,还会拓展到其他位置,比如let定义、关联类型等.

例如

use std::fmt::Debug; pub trait Fly { 
    fn fly(&self) ->bool; } #[derive(Debug)] struct Duck; #[derive(Debug)] struct Pig; impl Fly for Duck { 
    fn fly(&self)->bool{ 
    return true; } } impl Fly for Pig{ 
    fn fly(&self) -> bool{ 
    return false; } } fn fly_static(s: impl Fly+Debug) ->bool{ 
    s.fly() } fn can_fly(s:impl Fly+Debug) -> impl Fly{ 
    if s.fly(){ 
    println!("{:?} can fly",s); }else { 
    println!("{:?} can't fly",s); } s } fn main() { 
    let pig = Pig; assert_eq!(fly_static(pig),false); let pig = Pig; let pig = can_fly(pig); let duck = Duck; assert_eq!(fly_static(duck),true); let duck = Duck; let duck = can_fly(duck); } 

将impl Trait语法用于参数位置的时候,等价于使用trait限定的泛型。

let a: impl Trait 是不允许的。

另外,impl Trait只能用于为单个参数指定抽象类型,如果对多个参数使用implTrait语法,编译器将报错

use std::ops::Add; fn sum<T>(a:impl Add<Output = T>,b:impl Add<Output = T>)->T{ 
    a + b } 

impl 与 dyn是对应的,一个静态,一个动态。

标签trait

trait 这种对行为约束的特性也非常适合作为类型的标签。

Rust一共提供了5个重要的标签trait,都被定义在标准库std::marker模块中,分别为:

· Sized trait,用来标识编译期可确定大小的类型。
· Unsize trait,目前该trait为实验特性,用于标识动态大小类型(DST)。
· Copy trait,用来标识可以按位复制其值的类型。
· Send trait,用来标识可以跨线程安全通信的类型。
· Sync trait,用来标识可以在线程间安全共享引用的类型。

#[stable(feature = "rust1", since = "1.0.0")] #[lang = "sized"] pub trait Sized { 
    // Empty. } 

这里真正起“打标签”作用的是属性#[lang="sized"],该属性lang表示Sized trait供Rust语言本身使用,声明为"sized",称为语言项(Lang Item),这样编译器就知道Sized trait如何定义了。

默认为Sized,否则

struct Bar<T:?Sized>(T); 

Copy trait 继承Clone

#[lang = "copy"] pub trait Copy: Clone { 
    // Empty. } 
#[lang = "clone"] pub trait Clone: Sized { 
    fn clone(&self) -> Self; #[inline] #[stable(feature = "rust1", since = "1.0.0")] fn clone_from(&mut self, source: &Self) { 
    *self = source.clone() } } 

Rust为很多基本数据类型实现了Copy trait,比如常用的数字类型、字符(Char)、布尔类型、单元值、不可变引用等

Rust提供了Send和Sync两个标签trait,它们是Rust无数据竞争并发的基石。

类型转换

在编程语言中,类型转换分为隐式类型转换(Implicit Type Conversion)和显式类型转换(Explicit Type Conversion)。隐式类型转换是由编译器或解释器来完成的,开发者并未参与,所以又称之为强制类型转换(Type Coercion)。显式类型转换是由开发者指定的,就是一般意义上的类型转换(Type Cast)。

Deref解引用

Rust中的隐式类型转换基本上只有自动解引用。自动解引用的目的主要是方便开发者使用智能指针。Rust 中提供的 Box<T>、Rc<T>和 String 等类型,实际上是一种智能指针。

引用符&,解引用*

解引用可以自己实现,只要实现了Deref Trait 即可实现类型转换。

Deref的定义,

pub trait Deref { 
    type Target: ?Sized; fn deref(&self) -> &Self::Target; } 

DerefMut,返回的是可变应用。

解引用例子,字符串连接

fn main(){ 
    let s1 = "12".to_string(); let s2 = "34".to_string(); let s3 = s1 + &s2; println!("{}",s3); } 

s1,s2都是String类型,&s2应该是&String,期望的是&str,也就是说应该会报错,可是以上代码正常执行,就是因为String实现了解引用,

impl Deref for String { 
    type Target = str; fn deref(&self) -> &str{ 
    unsafe{ 
   str::from_utf8_unchecked(&self.vec)} } } 

标准库中常用的其他类型都实现了Deref,比如Vec<T>、Box<T>、Rc<T>、Arc<T>等。实现Deref的目的只有一个,就是简化编程。

解引用实例

use std::rc::Rc; fn test_vec(s:&[i32]){ 
    println!("{:?}",s); } fn test_rc(){ 
    let x = Rc::new("hello"); println!("{}",x); } fn main(){ 
    let v = vec![1,2,3]; test_vec(&v); test_rc(); } 

手动解引用

当某类型和其解引用目标类型中包含了相同的方法时,编译器就不知道该用哪一个了。此时就需要手动解引用,

fn test_deref(){ 
    let x = Rc::new("hello"); let y = x.clone();//&Rc<&str> let z = (*x).clone();//&str } 

因为Rc和str都实现了clone,所以不能自动解引用。

match需要手动解引用

fn test_str(){ 
    let s = "1234".to_string(); match &s[..]{ 
    "1234" => println!("hello"), _ => { 
   } } } 

有如下几种方式:

  • match x.deref()
  • match x.as_ref()
  • match x.borrow()
  • match &*x
  • match &x[…]
as 操作

as 操作符最常用的场景就是转换 Rust 中的基本数据类型。需要注意的是,as 关键字不支持重载。

fn main(){ 
    let a = 1u32; let b = a as u64; let c = 3i64; let d = c as u32; print!("a{},b{},c{},d{}",a,b,c,d); } 

长类型转短类型截断。

fn main(){ 
    let a = u32::MAX; let b = a as u16; println!("a:{},b:{}",a,b);//a:,b:65535 } 

as 还可以消除语法歧义。

#[derive(Debug)] struct C; trait A { 
    fn test(&self); } trait B { 
    fn test(&self); } impl A for C { 
    fn test(&self){ 
    println!("A:{:?}",self); } } impl B for C { 
    fn test(&self){ 
    println!("B:{:?}",self); } } fn main(){ 
    let c = C; A::test(&c); B::test(&c); <C as A>::test(&c); <C as B>::test(&c); } 
类型和子类型相互转换

as转换还可以用于类型和子类型之间的转换。Rust中没有标准定义中的子类型,比如结构体继承之类,但是生命周期标记可看作子类型。比如&'static str类型是&'a str类型的子类型,因为二者的生命周期标记不同,'a 和'static 都是生命周期标记,其中'a 是泛型标记,是&str的通用形式,而'static则是特指静态生命周期的&str字符串。

fn main(){ 
    let a:&'static str = "hello"; let b:& str = a as &str; let c:&'static str = b as &'static str; } 
From和Into

From和Into是定义于std::convert模块中的两个trait。它们定义了from和into两个方法,这两个方法互为反操作。

定义

pub trait From<T>: Sized { 
    /// Performs the conversion. #[lang = "from"] fn from(_: T) -> Self; } 
pub trait Into<T>: Sized { 
    /// Performs the conversion. #[must_use] fn into(self) -> T; } 

关于Into有一条默认的规则:如果类型U实现了From<T>,则T类型实例调用into方法就可以转换为类型U

因为Rust实现了

impl<T,U> Into<U> for T where U:From<T> 

当前trait系统的不足

· 孤儿规则的局限性。
· 代码复用的效率不高。
· 抽象表达能力有待改进。

到此这篇rust编程指南_rust与c的性能的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!

版权声明


相关文章:

  • rust教程视频_rust手机版2024-11-19 11:54:12
  • rust并发性能_rust和c++哪个难学2024-11-19 11:54:12
  • Rust语言系统编程实战(小北学习笔记)2024-11-19 11:54:12
  • rust web编程_rust权威指南第二版pdf2024-11-19 11:54:12
  • Java程序员学习Rust编程2024-11-19 11:54:12
  • rust 界面编程_rust和c++哪个难学2024-11-19 11:54:12
  • rust编程之道 电子书_rust入门教程2024-11-19 11:54:12
  • rust编程之道 电子书_rust值得学吗2024-11-19 11:54:12
  • Rust编程知识拾遗:Rust 编程,Option 学习2024-11-19 11:54:12
  • rust用法_rust值得学吗2024-11-19 11:54:12
  • 全屏图片